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#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
33pub struct Component {
34    pub name: String,
35    pub namespace: String,
36}
37
38/// Represents an endpoint with a namespace, component, and name.
39///
40/// An `Endpoint` is defined by a three-part string separated by `/` or a '.':
41/// - **namespace**
42/// - **component**
43/// - **name**
44///
45/// Example format: `"namespace/component/endpoint"`
46///
47/// TODO: There is also an Endpoint in runtime/src/component.rs
48#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
49pub struct Endpoint {
50    pub namespace: String,
51    pub component: String,
52    pub name: String,
53}
54
55impl PartialEq<Vec<&str>> for Endpoint {
56    fn eq(&self, other: &Vec<&str>) -> bool {
57        if other.len() != 3 {
58            return false;
59        }
60
61        self.namespace == other[0] && self.component == other[1] && self.name == other[2]
62    }
63}
64
65impl PartialEq<Endpoint> for Vec<&str> {
66    fn eq(&self, other: &Endpoint) -> bool {
67        other == self
68    }
69}
70
71impl Default for Endpoint {
72    fn default() -> Self {
73        Endpoint {
74            namespace: DEFAULT_NAMESPACE.to_string(),
75            component: DEFAULT_COMPONENT.to_string(),
76            name: DEFAULT_ENDPOINT.to_string(),
77        }
78    }
79}
80
81impl From<&str> for Endpoint {
82    /// Creates an `Endpoint` from a string.
83    ///
84    /// # Arguments
85    /// - `path`: A string in the format `"namespace/component/endpoint"`.
86    ///
87    /// The first two parts become the first two elements of the vector.
88    /// The third and subsequent parts are joined with '_' and become the third element.
89    /// Default values are used for missing parts.
90    ///
91    /// # Examples:
92    /// - "component" -> ["DEFAULT_NS", "component", "DEFAULT_E"]
93    /// - "namespace.component" -> ["namespace", "component", "DEFAULT_E"]
94    /// - "namespace.component.endpoint" -> ["namespace", "component", "endpoint"]
95    /// - "namespace/component" -> ["namespace", "component", "DEFAULT_E"]
96    /// - "namespace.component.endpoint.other.parts" -> ["namespace", "component", "endpoint_other_parts"]
97    ///
98    /// # Examples
99    /// ```ignore
100    /// use dynamo_runtime:protocols::Endpoint;
101    ///
102    /// let endpoint = Endpoint::from("namespace/component/endpoint");
103    /// assert_eq!(endpoint.namespace, "namespace");
104    /// assert_eq!(endpoint.component, "component");
105    /// assert_eq!(endpoint.name, "endpoint");
106    /// ```
107    fn from(input: &str) -> Self {
108        let mut result = Endpoint::default();
109
110        // Split the input string on either '.' or '/'
111        let elements: Vec<&str> = input
112            .trim_matches([' ', '/', '.'])
113            .split(['.', '/'])
114            .filter(|x| !x.is_empty())
115            .collect();
116
117        match elements.len() {
118            0 => {}
119            1 => {
120                result.component = elements[0].to_string();
121            }
122            2 => {
123                result.namespace = elements[0].to_string();
124                result.component = elements[1].to_string();
125            }
126            3 => {
127                result.namespace = elements[0].to_string();
128                result.component = elements[1].to_string();
129                result.name = elements[2].to_string();
130            }
131            x if x > 3 => {
132                result.namespace = elements[0].to_string();
133                result.component = elements[1].to_string();
134                result.name = elements[2..].join("_");
135            }
136            _ => unreachable!(),
137        }
138        result
139    }
140}
141
142impl FromStr for Endpoint {
143    type Err = PipelineError;
144
145    /// Parses an `Endpoint` from a string using the standard Rust `.parse::<T>()` pattern.
146    ///
147    /// This is implemented in terms of [`From<&str>`].
148    ///
149    /// # Errors
150    /// Does not fail
151    ///
152    /// # Examples
153    /// ```ignore
154    /// use std::str::FromStr;
155    /// use dynamo_runtime:protocols::Endpoint;
156    ///
157    /// let endpoint: Endpoint = "namespace/component/endpoint".parse().unwrap();
158    /// assert_eq!(endpoint.namespace, "namespace");
159    /// assert_eq!(endpoint.component, "component");
160    /// assert_eq!(endpoint.name, "endpoint");
161    /// ```
162    fn from_str(s: &str) -> Result<Self, Self::Err> {
163        Ok(Endpoint::from(s))
164    }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
168#[serde(rename_all = "snake_case")]
169pub enum RouterType {
170    PushRoundRobin,
171    PushRandom,
172}
173
174impl Default for RouterType {
175    fn default() -> Self {
176        Self::PushRandom
177    }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
181pub struct ModelMetaData {
182    pub name: String,
183    pub component: Component,
184    pub router_type: RouterType,
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_router_type_default() {
195        let default_router = RouterType::default();
196        assert_eq!(default_router, RouterType::PushRandom);
197    }
198
199    #[test]
200    fn test_router_type_serialization() {
201        let router_round_robin = RouterType::PushRoundRobin;
202        let router_random = RouterType::PushRandom;
203
204        let serialized_round_robin = serde_json::to_string(&router_round_robin).unwrap();
205        let serialized_random = serde_json::to_string(&router_random).unwrap();
206
207        assert_eq!(serialized_round_robin, "\"push_round_robin\"");
208        assert_eq!(serialized_random, "\"push_random\"");
209    }
210
211    #[test]
212    fn test_router_type_deserialization() {
213        let round_robin: RouterType = serde_json::from_str("\"push_round_robin\"").unwrap();
214        let random: RouterType = serde_json::from_str("\"push_random\"").unwrap();
215
216        assert_eq!(round_robin, RouterType::PushRoundRobin);
217        assert_eq!(random, RouterType::PushRandom);
218    }
219
220    #[test]
221    fn test_valid_endpoint_from() {
222        let input = "namespace1/component1/endpoint1";
223        let endpoint = Endpoint::from(input);
224
225        assert_eq!(endpoint.namespace, "namespace1");
226        assert_eq!(endpoint.component, "component1");
227        assert_eq!(endpoint.name, "endpoint1");
228    }
229
230    #[test]
231    fn test_valid_endpoint_from_str() {
232        let input = "namespace2/component2/endpoint2";
233        let endpoint = Endpoint::from_str(input).unwrap();
234
235        assert_eq!(endpoint.namespace, "namespace2");
236        assert_eq!(endpoint.component, "component2");
237        assert_eq!(endpoint.name, "endpoint2");
238    }
239
240    #[test]
241    fn test_valid_endpoint_parse() {
242        let input = "namespace3/component3/endpoint3";
243        let endpoint: Endpoint = input.parse().unwrap();
244
245        assert_eq!(endpoint.namespace, "namespace3");
246        assert_eq!(endpoint.component, "component3");
247        assert_eq!(endpoint.name, "endpoint3");
248    }
249
250    #[test]
251    fn test_endpoint_from() {
252        let result = Endpoint::from("component");
253        assert_eq!(
254            result,
255            vec![DEFAULT_NAMESPACE, "component", DEFAULT_ENDPOINT]
256        );
257    }
258
259    #[test]
260    fn test_namespace_component_endpoint() {
261        let result = Endpoint::from("namespace.component.endpoint");
262        assert_eq!(result, vec!["namespace", "component", "endpoint"]);
263    }
264
265    #[test]
266    fn test_forward_slash_separator() {
267        let result = Endpoint::from("namespace/component");
268        assert_eq!(result, vec!["namespace", "component", DEFAULT_ENDPOINT]);
269    }
270
271    #[test]
272    fn test_multiple_parts() {
273        let result = Endpoint::from("namespace.component.endpoint.other.parts");
274        assert_eq!(
275            result,
276            vec!["namespace", "component", "endpoint_other_parts"]
277        );
278    }
279
280    #[test]
281    fn test_mixed_separators() {
282        // Do it the .into way for variety and documentation
283        let result: Endpoint = "namespace/component.endpoint".into();
284        assert_eq!(result, vec!["namespace", "component", "endpoint"]);
285    }
286
287    #[test]
288    fn test_empty_string() {
289        let result = Endpoint::from("");
290        assert_eq!(
291            result,
292            vec![DEFAULT_NAMESPACE, DEFAULT_COMPONENT, DEFAULT_ENDPOINT]
293        );
294
295        // White space is equivalent to an empty string
296        let result = Endpoint::from("   ");
297        assert_eq!(
298            result,
299            vec![DEFAULT_NAMESPACE, DEFAULT_COMPONENT, DEFAULT_ENDPOINT]
300        );
301    }
302}