Skip to main content

dynamo_runtime/
protocols.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt;
5use std::str::FromStr;
6
7use serde::{Deserialize, Serialize};
8
9pub mod annotated;
10pub mod maybe_error;
11
12pub type LeaseId = i64;
13
14/// Default namespace if user does not provide one
15const DEFAULT_NAMESPACE: &str = "NS";
16
17const DEFAULT_COMPONENT: &str = "C";
18
19const DEFAULT_ENDPOINT: &str = "E";
20
21/// How we identify a namespace/component/endpoint URL.
22/// Technically the '://' is not part of the scheme but it eliminates several string
23/// concatenations.
24pub const ENDPOINT_SCHEME: &str = "dyn://";
25
26#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
27pub struct Component {
28    pub name: String,
29    pub namespace: String,
30}
31
32/// Represents an endpoint with a namespace, component, and name.
33///
34/// An [EndpointId] is defined by a three-part string separated by `/` or a '.':
35/// - **namespace**
36/// - **component**
37/// - **name**
38///
39/// Example format: `"namespace/component/endpoint"`
40///
41#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
42pub struct EndpointId {
43    pub namespace: String,
44    pub component: String,
45    pub name: String,
46}
47
48impl fmt::Display for EndpointId {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        write!(f, "{}/{}/{}", self.namespace, self.component, self.name)
51    }
52}
53
54impl PartialEq<Vec<&str>> for EndpointId {
55    fn eq(&self, other: &Vec<&str>) -> bool {
56        if other.len() != 3 {
57            return false;
58        }
59
60        self.namespace == other[0] && self.component == other[1] && self.name == other[2]
61    }
62}
63
64impl PartialEq<[&str; 3]> for EndpointId {
65    fn eq(&self, other: &[&str; 3]) -> bool {
66        self.namespace == other[0] && self.component == other[1] && self.name == other[2]
67    }
68}
69
70impl PartialEq<EndpointId> for [&str; 3] {
71    fn eq(&self, other: &EndpointId) -> bool {
72        other == self
73    }
74}
75
76impl PartialEq<EndpointId> for Vec<&str> {
77    fn eq(&self, other: &EndpointId) -> bool {
78        other == self
79    }
80}
81
82impl Default for EndpointId {
83    fn default() -> Self {
84        EndpointId {
85            namespace: DEFAULT_NAMESPACE.to_string(),
86            component: DEFAULT_COMPONENT.to_string(),
87            name: DEFAULT_ENDPOINT.to_string(),
88        }
89    }
90}
91
92impl From<&str> for EndpointId {
93    /// Creates an [EndpointId] from a string.
94    ///
95    /// # Arguments
96    /// - `path`: A string in the format `"namespace/component/endpoint"`.
97    ///
98    /// The first two parts become the first two elements of the vector.
99    /// The third and subsequent parts are joined with '_' and become the third element.
100    /// Default values are used for missing parts.
101    ///
102    /// # Examples:
103    /// - "component" -> ["DEFAULT_NAMESPACE", "component", "DEFAULT_ENDPOINT"]
104    /// - "namespace.component" -> ["namespace", "component", "DEFAULT_ENDPOINT"]
105    /// - "namespace.component.endpoint" -> ["namespace", "component", "endpoint"]
106    /// - "namespace/component" -> ["namespace", "component", "DEFAULT_ENDPOINT"]
107    /// - "namespace.component.endpoint.other.parts" -> ["namespace", "component", "endpoint_other_parts"]
108    ///
109    /// # Examples
110    /// ```
111    /// use dynamo_runtime::protocols::EndpointId;
112    ///
113    /// let endpoint = EndpointId::from("namespace/component/endpoint");
114    /// assert_eq!(endpoint.namespace, "namespace");
115    /// assert_eq!(endpoint.component, "component");
116    /// assert_eq!(endpoint.name, "endpoint");
117    /// ```
118    fn from(s: &str) -> Self {
119        let input = s.strip_prefix(ENDPOINT_SCHEME).unwrap_or(s);
120
121        // Split the input string on either '.' or '/'
122        let mut parts = input
123            .trim_matches([' ', '/', '.'])
124            .split(['.', '/'])
125            .filter(|x| !x.is_empty());
126
127        // Extract the first three potential components.
128        let p1 = parts.next();
129        let p2 = parts.next();
130        let p3 = parts.next();
131
132        let namespace;
133        let component;
134        let name;
135
136        match (p1, p2, p3) {
137            (None, _, _) => {
138                // 0 elements: all fields remain empty.
139                // Should this be an error?
140                namespace = DEFAULT_NAMESPACE.to_string();
141                component = DEFAULT_COMPONENT.to_string();
142                name = DEFAULT_ENDPOINT.to_string();
143            }
144            (Some(c), None, _) => {
145                namespace = DEFAULT_NAMESPACE.to_string();
146                component = c.to_string();
147                name = DEFAULT_ENDPOINT.to_string();
148            }
149            (Some(ns), Some(c), None) => {
150                // 2 elements: namespace, component
151                namespace = ns.to_string();
152                component = c.to_string();
153                name = DEFAULT_ENDPOINT.to_string();
154            }
155            (Some(ns), Some(c), Some(ep)) => {
156                namespace = ns.to_string();
157                component = c.to_string();
158
159                // For the 'name' field, we need to handle 'n' and any remaining parts.
160                // Instead of collecting into a Vec and then joining, we can build the string directly.
161                let mut endpoint_buf = String::from(ep); // Start with the third part
162                for part in parts {
163                    // 'parts' iterator continues from where p3 left off
164                    endpoint_buf.push('_');
165                    endpoint_buf.push_str(part);
166                }
167                name = endpoint_buf;
168            }
169        }
170
171        EndpointId {
172            namespace,
173            component,
174            name,
175        }
176    }
177}
178
179impl FromStr for EndpointId {
180    type Err = core::convert::Infallible;
181
182    /// Parses an `EndpointId` from a string using the standard Rust `.parse::<T>()` pattern.
183    ///
184    /// This is implemented in terms of [`From<&str>`].
185    ///
186    /// # Errors
187    /// Does not fail
188    ///
189    /// # Examples
190    /// ```
191    /// use std::str::FromStr;
192    /// use dynamo_runtime::protocols::EndpointId;
193    ///
194    /// let endpoint: EndpointId = "namespace/component/endpoint".parse().unwrap();
195    /// assert_eq!(endpoint.namespace, "namespace");
196    /// assert_eq!(endpoint.component, "component");
197    /// assert_eq!(endpoint.name, "endpoint");
198    /// let endpoint: EndpointId = "dyn://namespace/component/endpoint".parse().unwrap();
199    /// // same as above
200    /// assert_eq!(endpoint.name, "endpoint");
201    /// ```
202    fn from_str(s: &str) -> Result<Self, Self::Err> {
203        Ok(EndpointId::from(s))
204    }
205}
206
207impl EndpointId {
208    /// As a String like dyn://dynamo.internal.worker
209    pub fn as_url(&self) -> String {
210        format!(
211            "{ENDPOINT_SCHEME}{}.{}.{}",
212            self.namespace, self.component, self.name
213        )
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use std::convert::TryFrom;
221    use std::str::FromStr;
222
223    #[test]
224    fn test_valid_endpoint_from() {
225        let input = "namespace1/component1/endpoint1";
226        let endpoint = EndpointId::from(input);
227
228        assert_eq!(endpoint.namespace, "namespace1");
229        assert_eq!(endpoint.component, "component1");
230        assert_eq!(endpoint.name, "endpoint1");
231    }
232
233    #[test]
234    fn test_valid_endpoint_from_str() {
235        let input = "namespace2/component2/endpoint2";
236        let endpoint = EndpointId::from_str(input).unwrap();
237
238        assert_eq!(endpoint.namespace, "namespace2");
239        assert_eq!(endpoint.component, "component2");
240        assert_eq!(endpoint.name, "endpoint2");
241    }
242
243    #[test]
244    fn test_valid_endpoint_parse() {
245        let input = "namespace3/component3/endpoint3";
246        let endpoint: EndpointId = input.parse().unwrap();
247
248        assert_eq!(endpoint.namespace, "namespace3");
249        assert_eq!(endpoint.component, "component3");
250        assert_eq!(endpoint.name, "endpoint3");
251    }
252
253    #[test]
254    fn test_endpoint_from() {
255        let result = EndpointId::from("component");
256        assert_eq!(
257            result,
258            vec![DEFAULT_NAMESPACE, "component", DEFAULT_ENDPOINT]
259        );
260    }
261
262    #[test]
263    fn test_namespace_component_endpoint() {
264        let result = EndpointId::from("namespace.component.endpoint");
265        assert_eq!(result, vec!["namespace", "component", "endpoint"]);
266    }
267
268    #[test]
269    fn test_forward_slash_separator() {
270        let result = EndpointId::from("namespace/component");
271        assert_eq!(result, vec!["namespace", "component", DEFAULT_ENDPOINT]);
272    }
273
274    #[test]
275    fn test_multiple_parts() {
276        let result = EndpointId::from("namespace.component.endpoint.other.parts");
277        assert_eq!(
278            result,
279            vec!["namespace", "component", "endpoint_other_parts"]
280        );
281    }
282
283    #[test]
284    fn test_mixed_separators() {
285        // Do it the .into way for variety and documentation
286        let result: EndpointId = "namespace/component.endpoint".into();
287        assert_eq!(result, vec!["namespace", "component", "endpoint"]);
288    }
289
290    #[test]
291    fn test_empty_string() {
292        let result = EndpointId::from("");
293        assert_eq!(
294            result,
295            vec![DEFAULT_NAMESPACE, DEFAULT_COMPONENT, DEFAULT_ENDPOINT]
296        );
297
298        // White space is equivalent to an empty string
299        let result = EndpointId::from("   ");
300        assert_eq!(
301            result,
302            vec![DEFAULT_NAMESPACE, DEFAULT_COMPONENT, DEFAULT_ENDPOINT]
303        );
304    }
305
306    #[test]
307    fn test_parse_with_scheme_and_url_roundtrip() {
308        let input = "dyn://ns/cp/ep";
309        let endpoint: EndpointId = input.parse().unwrap();
310        assert_eq!(endpoint, vec!["ns", "cp", "ep"]);
311        assert_eq!(endpoint.as_url(), "dyn://ns.cp.ep");
312    }
313}