dynamo_runtime/
protocols.rs

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