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//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use serde::{Deserialize, Serialize};
17use std::str::FromStr;
18
19use crate::pipeline::PipelineError;
20
21pub mod annotated;
22
23pub type LeaseId = i64;
24
25/// Default namespace if user does not provide one
26const DEFAULT_NAMESPACE: &str = "NS";
27
28const DEFAULT_COMPONENT: &str = "C";
29
30const DEFAULT_ENDPOINT: &str = "E";
31
32/// How we identify a namespace/component/endpoint URL.
33/// Technically the '://' is not part of the scheme but it eliminates several string
34/// concatenations.
35pub const ENDPOINT_SCHEME: &str = "dyn://";
36
37#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
38pub struct Component {
39    pub name: String,
40    pub namespace: String,
41}
42
43/// Represents an endpoint with a namespace, component, and name.
44///
45/// An `Endpoint` is defined by a three-part string separated by `/` or a '.':
46/// - **namespace**
47/// - **component**
48/// - **name**
49///
50/// Example format: `"namespace/component/endpoint"`
51///
52/// TODO: There is also an Endpoint in runtime/src/component.rs
53#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
54pub struct Endpoint {
55    pub namespace: String,
56    pub component: String,
57    pub name: String,
58}
59
60impl PartialEq<Vec<&str>> for Endpoint {
61    fn eq(&self, other: &Vec<&str>) -> bool {
62        if other.len() != 3 {
63            return false;
64        }
65
66        self.namespace == other[0] && self.component == other[1] && self.name == other[2]
67    }
68}
69
70impl PartialEq<Endpoint> for Vec<&str> {
71    fn eq(&self, other: &Endpoint) -> bool {
72        other == self
73    }
74}
75
76impl Default for Endpoint {
77    fn default() -> Self {
78        Endpoint {
79            namespace: DEFAULT_NAMESPACE.to_string(),
80            component: DEFAULT_COMPONENT.to_string(),
81            name: DEFAULT_ENDPOINT.to_string(),
82        }
83    }
84}
85
86impl From<&str> for Endpoint {
87    /// Creates an `Endpoint` from a string.
88    ///
89    /// # Arguments
90    /// - `path`: A string in the format `"namespace/component/endpoint"`.
91    ///
92    /// The first two parts become the first two elements of the vector.
93    /// The third and subsequent parts are joined with '_' and become the third element.
94    /// Default values are used for missing parts.
95    ///
96    /// # Examples:
97    /// - "component" -> ["DEFAULT_NS", "component", "DEFAULT_E"]
98    /// - "namespace.component" -> ["namespace", "component", "DEFAULT_E"]
99    /// - "namespace.component.endpoint" -> ["namespace", "component", "endpoint"]
100    /// - "namespace/component" -> ["namespace", "component", "DEFAULT_E"]
101    /// - "namespace.component.endpoint.other.parts" -> ["namespace", "component", "endpoint_other_parts"]
102    ///
103    /// # Examples
104    /// ```ignore
105    /// use dynamo_runtime:protocols::Endpoint;
106    ///
107    /// let endpoint = Endpoint::from("namespace/component/endpoint");
108    /// assert_eq!(endpoint.namespace, "namespace");
109    /// assert_eq!(endpoint.component, "component");
110    /// assert_eq!(endpoint.name, "endpoint");
111    /// ```
112    fn from(input: &str) -> Self {
113        let mut result = Endpoint::default();
114
115        // Split the input string on either '.' or '/'
116        let elements: Vec<&str> = input
117            .trim_matches([' ', '/', '.'])
118            .split(['.', '/'])
119            .filter(|x| !x.is_empty())
120            .collect();
121
122        match elements.len() {
123            0 => {}
124            1 => {
125                result.component = elements[0].to_string();
126            }
127            2 => {
128                result.namespace = elements[0].to_string();
129                result.component = elements[1].to_string();
130            }
131            3 => {
132                result.namespace = elements[0].to_string();
133                result.component = elements[1].to_string();
134                result.name = elements[2].to_string();
135            }
136            x if x > 3 => {
137                result.namespace = elements[0].to_string();
138                result.component = elements[1].to_string();
139                result.name = elements[2..].join("_");
140            }
141            _ => unreachable!(),
142        }
143        result
144    }
145}
146
147impl FromStr for Endpoint {
148    type Err = PipelineError;
149
150    /// Parses an `Endpoint` from a string using the standard Rust `.parse::<T>()` pattern.
151    ///
152    /// This is implemented in terms of [`From<&str>`].
153    ///
154    /// # Errors
155    /// Does not fail
156    ///
157    /// # Examples
158    /// ```ignore
159    /// use std::str::FromStr;
160    /// use dynamo_runtime:protocols::Endpoint;
161    ///
162    /// let endpoint: Endpoint = "namespace/component/endpoint".parse().unwrap();
163    /// assert_eq!(endpoint.namespace, "namespace");
164    /// assert_eq!(endpoint.component, "component");
165    /// assert_eq!(endpoint.name, "endpoint");
166    /// let endpoint: Endpoint = "dyn://namespace/component/endpoint".parse().unwrap();
167    /// // same as above
168    /// assert_eq!(endpoint.name, "endpoint");
169    /// ```
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        let cleaned = s.strip_prefix(ENDPOINT_SCHEME).unwrap_or(s);
172        Ok(Endpoint::from(cleaned))
173    }
174}
175
176impl Endpoint {
177    /// As a String like dyn://dynamo.internal.worker
178    pub fn as_url(&self) -> String {
179        format!(
180            "{ENDPOINT_SCHEME}{}.{}.{}",
181            self.namespace, self.component, self.name
182        )
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use std::convert::TryFrom;
190    use std::str::FromStr;
191
192    #[test]
193    fn test_valid_endpoint_from() {
194        let input = "namespace1/component1/endpoint1";
195        let endpoint = Endpoint::from(input);
196
197        assert_eq!(endpoint.namespace, "namespace1");
198        assert_eq!(endpoint.component, "component1");
199        assert_eq!(endpoint.name, "endpoint1");
200    }
201
202    #[test]
203    fn test_valid_endpoint_from_str() {
204        let input = "namespace2/component2/endpoint2";
205        let endpoint = Endpoint::from_str(input).unwrap();
206
207        assert_eq!(endpoint.namespace, "namespace2");
208        assert_eq!(endpoint.component, "component2");
209        assert_eq!(endpoint.name, "endpoint2");
210    }
211
212    #[test]
213    fn test_valid_endpoint_parse() {
214        let input = "namespace3/component3/endpoint3";
215        let endpoint: Endpoint = input.parse().unwrap();
216
217        assert_eq!(endpoint.namespace, "namespace3");
218        assert_eq!(endpoint.component, "component3");
219        assert_eq!(endpoint.name, "endpoint3");
220    }
221
222    #[test]
223    fn test_endpoint_from() {
224        let result = Endpoint::from("component");
225        assert_eq!(
226            result,
227            vec![DEFAULT_NAMESPACE, "component", DEFAULT_ENDPOINT]
228        );
229    }
230
231    #[test]
232    fn test_namespace_component_endpoint() {
233        let result = Endpoint::from("namespace.component.endpoint");
234        assert_eq!(result, vec!["namespace", "component", "endpoint"]);
235    }
236
237    #[test]
238    fn test_forward_slash_separator() {
239        let result = Endpoint::from("namespace/component");
240        assert_eq!(result, vec!["namespace", "component", DEFAULT_ENDPOINT]);
241    }
242
243    #[test]
244    fn test_multiple_parts() {
245        let result = Endpoint::from("namespace.component.endpoint.other.parts");
246        assert_eq!(
247            result,
248            vec!["namespace", "component", "endpoint_other_parts"]
249        );
250    }
251
252    #[test]
253    fn test_mixed_separators() {
254        // Do it the .into way for variety and documentation
255        let result: Endpoint = "namespace/component.endpoint".into();
256        assert_eq!(result, vec!["namespace", "component", "endpoint"]);
257    }
258
259    #[test]
260    fn test_empty_string() {
261        let result = Endpoint::from("");
262        assert_eq!(
263            result,
264            vec![DEFAULT_NAMESPACE, DEFAULT_COMPONENT, DEFAULT_ENDPOINT]
265        );
266
267        // White space is equivalent to an empty string
268        let result = Endpoint::from("   ");
269        assert_eq!(
270            result,
271            vec![DEFAULT_NAMESPACE, DEFAULT_COMPONENT, DEFAULT_ENDPOINT]
272        );
273    }
274}