Skip to main content

trellis_core/
resource_key.rs

1use core::fmt;
2
3const MULTI_SEGMENT_PREFIX: &str = "segments:";
4const ESCAPED_SINGLE_SEGMENT_PREFIX: &str = "segment:";
5
6/// Stable identity for a desired external resource.
7#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
8pub struct ResourceKey {
9    inner: Box<ResourceKeyInner>,
10}
11
12#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
13struct ResourceKeyInner {
14    segments: Box<[Box<str>]>,
15    encoded: Box<str>,
16}
17
18impl ResourceKey {
19    /// Creates a single-segment resource key from deterministic host-chosen identity.
20    pub fn new(key: impl Into<Box<str>>) -> Self {
21        Self::from_boxed_segments(vec![key.into()].into_boxed_slice())
22    }
23
24    /// Creates a resource key from ordered identity segments.
25    ///
26    /// Prefer this for product identifiers with multiple parts. Hosts can recover
27    /// the exact segments from close commands without parsing a flattened string.
28    ///
29    /// # Panics
30    ///
31    /// Panics when called with no segments. Use [`Self::try_from_segments`] when
32    /// the segment list may be empty.
33    pub fn from_segments<I, S>(segments: I) -> Self
34    where
35        I: IntoIterator<Item = S>,
36        S: Into<Box<str>>,
37    {
38        Self::try_from_segments(segments).expect("resource keys require at least one segment")
39    }
40
41    /// Creates a resource key from ordered identity segments, returning `None`
42    /// when the segment list is empty.
43    pub fn try_from_segments<I, S>(segments: I) -> Option<Self>
44    where
45        I: IntoIterator<Item = S>,
46        S: Into<Box<str>>,
47    {
48        let segments = segments
49            .into_iter()
50            .map(Into::into)
51            .collect::<Vec<_>>()
52            .into_boxed_slice();
53        (!segments.is_empty()).then(|| Self::from_boxed_segments(segments))
54    }
55
56    /// Creates an explicit broad-resource key.
57    ///
58    /// Core treats this as an opaque identity; tests and applications decide
59    /// whether the key represents a forbidden fallback or wildcard resource.
60    pub fn wildcard(key: impl AsRef<str>) -> Self {
61        Self::from_segments(["wildcard", key.as_ref()])
62    }
63
64    /// Returns this key's deterministic encoded representation.
65    ///
66    /// Use [`Self::segments`] or [`Self::segment`] when application code needs
67    /// product identity back from a resource command. Single-segment keys return
68    /// the segment directly unless it is in core's reserved diagnostic namespace.
69    pub fn as_str(&self) -> &str {
70        &self.inner.encoded
71    }
72
73    /// Returns this key's ordered identity segments.
74    pub fn segments(&self) -> impl ExactSizeIterator<Item = &str> + '_ {
75        self.inner.segments.iter().map(|segment| segment.as_ref())
76    }
77
78    /// Returns one identity segment by index.
79    pub fn segment(&self, index: usize) -> Option<&str> {
80        self.inner
81            .segments
82            .get(index)
83            .map(|segment| segment.as_ref())
84    }
85
86    /// Returns the number of identity segments.
87    pub fn segment_count(&self) -> usize {
88        self.inner.segments.len()
89    }
90
91    fn from_boxed_segments(segments: Box<[Box<str>]>) -> Self {
92        let encoded = encode_segments(&segments);
93        Self {
94            inner: Box::new(ResourceKeyInner { segments, encoded }),
95        }
96    }
97}
98
99impl fmt::Debug for ResourceKey {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        let segments = self.segments().collect::<Vec<_>>();
102        f.debug_tuple("ResourceKey").field(&segments).finish()
103    }
104}
105
106fn encode_segments(segments: &[Box<str>]) -> Box<str> {
107    if let [segment] = segments {
108        if segment.starts_with(MULTI_SEGMENT_PREFIX)
109            || segment.starts_with(ESCAPED_SINGLE_SEGMENT_PREFIX)
110        {
111            return encode_single_segment(segment).into_boxed_str();
112        }
113        return segment.clone();
114    }
115
116    let mut encoded = String::from(MULTI_SEGMENT_PREFIX);
117    encoded.push_str(&segments.len().to_string());
118    encoded.push(':');
119    for segment in segments {
120        encoded.push_str(&segment.len().to_string());
121        encoded.push(':');
122        encoded.push_str(segment);
123    }
124    encoded.into_boxed_str()
125}
126
127fn encode_single_segment(segment: &str) -> String {
128    let mut encoded = String::from(ESCAPED_SINGLE_SEGMENT_PREFIX);
129    encoded.push_str(&segment.len().to_string());
130    encoded.push(':');
131    encoded.push_str(segment);
132    encoded
133}
134
135#[cfg(feature = "serde")]
136impl serde::Serialize for ResourceKey {
137    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138    where
139        S: serde::Serializer,
140    {
141        if let [segment] = self.inner.segments.as_ref() {
142            serializer.serialize_str(segment)
143        } else {
144            serializer.collect_seq(self.segments())
145        }
146    }
147}
148
149#[cfg(feature = "serde")]
150impl<'de> serde::Deserialize<'de> for ResourceKey {
151    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
152    where
153        D: serde::Deserializer<'de>,
154    {
155        #[derive(serde::Deserialize)]
156        #[serde(untagged)]
157        enum EncodedResourceKey {
158            Single(String),
159            Segments(Vec<String>),
160        }
161
162        match EncodedResourceKey::deserialize(deserializer)? {
163            EncodedResourceKey::Single(segment) => Ok(Self::new(segment)),
164            EncodedResourceKey::Segments(segments) => {
165                Self::try_from_segments(segments).ok_or_else(|| {
166                    serde::de::Error::custom("resource key must contain at least one segment")
167                })
168            }
169        }
170    }
171}