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