protocols.rs 9.33 KB
Newer Older
1
2
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
Ryan Olson's avatar
Ryan Olson committed
3
4

use serde::{Deserialize, Serialize};
5
6
use std::str::FromStr;

Ryan Olson's avatar
Ryan Olson committed
7
pub mod annotated;
8
pub mod maybe_error;
Ryan Olson's avatar
Ryan Olson committed
9

10
11
pub type LeaseId = i64;

12
13
14
15
16
17
18
/// Default namespace if user does not provide one
const DEFAULT_NAMESPACE: &str = "NS";

const DEFAULT_COMPONENT: &str = "C";

const DEFAULT_ENDPOINT: &str = "E";

19
20
21
22
23
/// How we identify a namespace/component/endpoint URL.
/// Technically the '://' is not part of the scheme but it eliminates several string
/// concatenations.
pub const ENDPOINT_SCHEME: &str = "dyn://";

Ryan Olson's avatar
Ryan Olson committed
24
25
26
27
28
29
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct Component {
    pub name: String,
    pub namespace: String,
}

30
31
/// Represents an endpoint with a namespace, component, and name.
///
32
/// An [EndpointId] is defined by a three-part string separated by `/` or a '.':
33
34
35
36
37
/// - **namespace**
/// - **component**
/// - **name**
///
/// Example format: `"namespace/component/endpoint"`
38
///
Ryan Olson's avatar
Ryan Olson committed
39
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
40
pub struct EndpointId {
41
42
    pub namespace: String,
    pub component: String,
Ryan Olson's avatar
Ryan Olson committed
43
    pub name: String,
44
}
45

46
impl PartialEq<Vec<&str>> for EndpointId {
47
48
49
50
    fn eq(&self, other: &Vec<&str>) -> bool {
        if other.len() != 3 {
            return false;
        }
51

52
53
54
55
        self.namespace == other[0] && self.component == other[1] && self.name == other[2]
    }
}

56
57
58
59
60
61
62
63
64
65
66
67
68
69
impl PartialEq<[&str; 3]> for EndpointId {
    fn eq(&self, other: &[&str; 3]) -> bool {
        self.namespace == other[0] && self.component == other[1] && self.name == other[2]
    }
}

impl PartialEq<EndpointId> for [&str; 3] {
    fn eq(&self, other: &EndpointId) -> bool {
        other == self
    }
}

impl PartialEq<EndpointId> for Vec<&str> {
    fn eq(&self, other: &EndpointId) -> bool {
70
71
        other == self
    }
Ryan Olson's avatar
Ryan Olson committed
72
73
}

74
impl Default for EndpointId {
75
    fn default() -> Self {
76
        EndpointId {
77
78
79
80
81
82
            namespace: DEFAULT_NAMESPACE.to_string(),
            component: DEFAULT_COMPONENT.to_string(),
            name: DEFAULT_ENDPOINT.to_string(),
        }
    }
}
83

84
85
impl From<&str> for EndpointId {
    /// Creates an [EndpointId] from a string.
86
87
88
89
    ///
    /// # Arguments
    /// - `path`: A string in the format `"namespace/component/endpoint"`.
    ///
90
91
92
93
94
    /// The first two parts become the first two elements of the vector.
    /// The third and subsequent parts are joined with '_' and become the third element.
    /// Default values are used for missing parts.
    ///
    /// # Examples:
95
96
    /// - "component" -> ["DEFAULT_NAMESPACE", "component", "DEFAULT_ENDPOINT"]
    /// - "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
107
108
109
    /// assert_eq!(endpoint.namespace, "namespace");
    /// assert_eq!(endpoint.component, "component");
    /// assert_eq!(endpoint.name, "endpoint");
    /// ```
110
111
    fn from(s: &str) -> Self {
        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
116
            .trim_matches([' ', '/', '.'])
            .split(['.', '/'])
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
            .filter(|x| !x.is_empty());

        // Extract the first three potential components.
        let p1 = parts.next();
        let p2 = parts.next();
        let p3 = parts.next();

        let namespace;
        let component;
        let name;

        match (p1, p2, p3) {
            (None, _, _) => {
                // 0 elements: all fields remain empty.
                // Should this be an error?
                namespace = DEFAULT_NAMESPACE.to_string();
                component = DEFAULT_COMPONENT.to_string();
                name = DEFAULT_ENDPOINT.to_string();
135
            }
136
137
138
139
            (Some(c), None, _) => {
                namespace = DEFAULT_NAMESPACE.to_string();
                component = c.to_string();
                name = DEFAULT_ENDPOINT.to_string();
140
            }
141
142
143
144
145
            (Some(ns), Some(c), None) => {
                // 2 elements: namespace, component
                namespace = ns.to_string();
                component = c.to_string();
                name = DEFAULT_ENDPOINT.to_string();
146
            }
147
148
149
150
151
152
153
154
155
156
157
158
159
            (Some(ns), Some(c), Some(ep)) => {
                namespace = ns.to_string();
                component = c.to_string();

                // For the 'name' field, we need to handle 'n' and any remaining parts.
                // Instead of collecting into a Vec and then joining, we can build the string directly.
                let mut endpoint_buf = String::from(ep); // Start with the third part
                for part in parts {
                    // 'parts' iterator continues from where p3 left off
                    endpoint_buf.push('_');
                    endpoint_buf.push_str(part);
                }
                name = endpoint_buf;
160
            }
161
        }
162
163
164
165
166
167

        EndpointId {
            namespace,
            component,
            name,
        }
168
169
170
    }
}

171
172
impl FromStr for EndpointId {
    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
188
189
    /// assert_eq!(endpoint.namespace, "namespace");
    /// assert_eq!(endpoint.component, "component");
    /// assert_eq!(endpoint.name, "endpoint");
190
    /// let endpoint: EndpointId = "dyn://namespace/component/endpoint".parse().unwrap();
191
192
    /// // same as above
    /// assert_eq!(endpoint.name, "endpoint");
193
194
    /// ```
    fn from_str(s: &str) -> Result<Self, Self::Err> {
195
        Ok(EndpointId::from(s))
196
197
198
    }
}

199
impl EndpointId {
200
201
202
203
204
205
206
207
208
    /// As a String like dyn://dynamo.internal.worker
    pub fn as_url(&self) -> String {
        format!(
            "{ENDPOINT_SCHEME}{}.{}.{}",
            self.namespace, self.component, self.name
        )
    }
}

209
210
211
#[cfg(test)]
mod tests {
    use super::*;
212
213
    use std::convert::TryFrom;
    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
221
222
223
224
225
226
227

        assert_eq!(endpoint.namespace, "namespace1");
        assert_eq!(endpoint.component, "component1");
        assert_eq!(endpoint.name, "endpoint1");
    }

    #[test]
    fn test_valid_endpoint_from_str() {
        let input = "namespace2/component2/endpoint2";
228
        let endpoint = EndpointId::from_str(input).unwrap();
229
230
231
232
233
234
235
236
237

        assert_eq!(endpoint.namespace, "namespace2");
        assert_eq!(endpoint.component, "component2");
        assert_eq!(endpoint.name, "endpoint2");
    }

    #[test]
    fn test_valid_endpoint_parse() {
        let input = "namespace3/component3/endpoint3";
238
        let endpoint: EndpointId = input.parse().unwrap();
239
240
241
242
243
244
245

        assert_eq!(endpoint.namespace, "namespace3");
        assert_eq!(endpoint.component, "component3");
        assert_eq!(endpoint.name, "endpoint3");
    }

    #[test]
246
    fn test_endpoint_from() {
247
        let result = EndpointId::from("component");
248
        assert_eq!(
249
250
            result,
            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
271
            result,
            vec!["namespace", "component", "endpoint_other_parts"]
272
273
274
275
        );
    }

    #[test]
276
277
    fn test_mixed_separators() {
        // 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
286
287
288
        assert_eq!(
            result,
            vec![DEFAULT_NAMESPACE, DEFAULT_COMPONENT, DEFAULT_ENDPOINT]
        );
289

290
        // White space is equivalent to an empty string
291
        let result = EndpointId::from("   ");
292
293
294
        assert_eq!(
            result,
            vec![DEFAULT_NAMESPACE, DEFAULT_COMPONENT, DEFAULT_ENDPOINT]
295
296
        );
    }
297
298
299
300
301
302
303
304

    #[test]
    fn test_parse_with_scheme_and_url_roundtrip() {
        let input = "dyn://ns/cp/ep";
        let endpoint: EndpointId = input.parse().unwrap();
        assert_eq!(endpoint, vec!["ns", "cp", "ep"]);
        assert_eq!(endpoint.as_url(), "dyn://ns.cp.ep");
    }
305
}