protocols.rs 8.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Ryan Olson's avatar
Ryan Olson committed
15
16

use serde::{Deserialize, Serialize};
17
18
19
use std::str::FromStr;

use crate::pipeline::PipelineError;
Ryan Olson's avatar
Ryan Olson committed
20
21

pub mod annotated;
22
pub mod maybe_error;
Ryan Olson's avatar
Ryan Olson committed
23

24
25
pub type LeaseId = i64;

26
27
28
29
30
31
32
/// Default namespace if user does not provide one
const DEFAULT_NAMESPACE: &str = "NS";

const DEFAULT_COMPONENT: &str = "C";

const DEFAULT_ENDPOINT: &str = "E";

33
34
35
36
37
/// 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
38
39
40
41
42
43
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct Component {
    pub name: String,
    pub namespace: String,
}

44
45
/// Represents an endpoint with a namespace, component, and name.
///
46
/// An `Endpoint` is defined by a three-part string separated by `/` or a '.':
47
48
49
50
51
/// - **namespace**
/// - **component**
/// - **name**
///
/// Example format: `"namespace/component/endpoint"`
52
53
///
/// TODO: There is also an Endpoint in runtime/src/component.rs
Ryan Olson's avatar
Ryan Olson committed
54
55
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct Endpoint {
56
57
    pub namespace: String,
    pub component: String,
Ryan Olson's avatar
Ryan Olson committed
58
    pub name: String,
59
}
60

61
62
63
64
65
impl PartialEq<Vec<&str>> for Endpoint {
    fn eq(&self, other: &Vec<&str>) -> bool {
        if other.len() != 3 {
            return false;
        }
66

67
68
69
70
71
72
73
74
        self.namespace == other[0] && self.component == other[1] && self.name == other[2]
    }
}

impl PartialEq<Endpoint> for Vec<&str> {
    fn eq(&self, other: &Endpoint) -> bool {
        other == self
    }
Ryan Olson's avatar
Ryan Olson committed
75
76
}

77
78
79
80
81
82
83
84
85
impl Default for Endpoint {
    fn default() -> Self {
        Endpoint {
            namespace: DEFAULT_NAMESPACE.to_string(),
            component: DEFAULT_COMPONENT.to_string(),
            name: DEFAULT_ENDPOINT.to_string(),
        }
    }
}
86

87
88
impl From<&str> for Endpoint {
    /// Creates an `Endpoint` from a string.
89
90
91
92
    ///
    /// # Arguments
    /// - `path`: A string in the format `"namespace/component/endpoint"`.
    ///
93
94
95
96
97
98
99
100
101
102
    /// 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:
    /// - "component" -> ["DEFAULT_NS", "component", "DEFAULT_E"]
    /// - "namespace.component" -> ["namespace", "component", "DEFAULT_E"]
    /// - "namespace.component.endpoint" -> ["namespace", "component", "endpoint"]
    /// - "namespace/component" -> ["namespace", "component", "DEFAULT_E"]
    /// - "namespace.component.endpoint.other.parts" -> ["namespace", "component", "endpoint_other_parts"]
103
104
105
    ///
    /// # Examples
    /// ```ignore
Neelay Shah's avatar
Neelay Shah committed
106
    /// use dynamo_runtime:protocols::Endpoint;
107
    ///
108
    /// let endpoint = Endpoint::from("namespace/component/endpoint");
109
110
111
112
    /// assert_eq!(endpoint.namespace, "namespace");
    /// assert_eq!(endpoint.component, "component");
    /// assert_eq!(endpoint.name, "endpoint");
    /// ```
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
    fn from(input: &str) -> Self {
        let mut result = Endpoint::default();

        // Split the input string on either '.' or '/'
        let elements: Vec<&str> = input
            .trim_matches([' ', '/', '.'])
            .split(['.', '/'])
            .filter(|x| !x.is_empty())
            .collect();

        match elements.len() {
            0 => {}
            1 => {
                result.component = elements[0].to_string();
            }
            2 => {
                result.namespace = elements[0].to_string();
                result.component = elements[1].to_string();
            }
            3 => {
                result.namespace = elements[0].to_string();
                result.component = elements[1].to_string();
                result.name = elements[2].to_string();
            }
            x if x > 3 => {
                result.namespace = elements[0].to_string();
                result.component = elements[1].to_string();
                result.name = elements[2..].join("_");
            }
            _ => unreachable!(),
143
        }
144
        result
145
146
147
148
149
150
151
152
    }
}

impl FromStr for Endpoint {
    type Err = PipelineError;

    /// Parses an `Endpoint` from a string using the standard Rust `.parse::<T>()` pattern.
    ///
153
    /// This is implemented in terms of [`From<&str>`].
154
155
    ///
    /// # Errors
156
    /// Does not fail
157
158
159
160
    ///
    /// # Examples
    /// ```ignore
    /// use std::str::FromStr;
Neelay Shah's avatar
Neelay Shah committed
161
    /// use dynamo_runtime:protocols::Endpoint;
162
163
164
165
166
    ///
    /// let endpoint: Endpoint = "namespace/component/endpoint".parse().unwrap();
    /// assert_eq!(endpoint.namespace, "namespace");
    /// assert_eq!(endpoint.component, "component");
    /// assert_eq!(endpoint.name, "endpoint");
167
168
169
    /// let endpoint: Endpoint = "dyn://namespace/component/endpoint".parse().unwrap();
    /// // same as above
    /// assert_eq!(endpoint.name, "endpoint");
170
171
    /// ```
    fn from_str(s: &str) -> Result<Self, Self::Err> {
172
173
        let cleaned = s.strip_prefix(ENDPOINT_SCHEME).unwrap_or(s);
        Ok(Endpoint::from(cleaned))
174
175
176
    }
}

177
178
179
180
181
182
183
184
185
186
impl Endpoint {
    /// As a String like dyn://dynamo.internal.worker
    pub fn as_url(&self) -> String {
        format!(
            "{ENDPOINT_SCHEME}{}.{}.{}",
            self.namespace, self.component, self.name
        )
    }
}

187
188
189
#[cfg(test)]
mod tests {
    use super::*;
190
191
    use std::convert::TryFrom;
    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
199
200
201
202
203
204
205

        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";
206
        let endpoint = Endpoint::from_str(input).unwrap();
207
208
209
210
211
212
213
214
215

        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";
216
        let endpoint: Endpoint = input.parse().unwrap();
217
218
219
220
221
222
223

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

    #[test]
224
225
    fn test_endpoint_from() {
        let result = Endpoint::from("component");
226
        assert_eq!(
227
228
            result,
            vec![DEFAULT_NAMESPACE, "component", DEFAULT_ENDPOINT]
229
230
231
232
        );
    }

    #[test]
233
234
235
    fn test_namespace_component_endpoint() {
        let result = Endpoint::from("namespace.component.endpoint");
        assert_eq!(result, vec!["namespace", "component", "endpoint"]);
236
237
238
    }

    #[test]
239
240
241
    fn test_forward_slash_separator() {
        let result = Endpoint::from("namespace/component");
        assert_eq!(result, vec!["namespace", "component", DEFAULT_ENDPOINT]);
242
243
244
    }

    #[test]
245
246
    fn test_multiple_parts() {
        let result = Endpoint::from("namespace.component.endpoint.other.parts");
247
        assert_eq!(
248
249
            result,
            vec!["namespace", "component", "endpoint_other_parts"]
250
251
252
253
        );
    }

    #[test]
254
255
256
257
    fn test_mixed_separators() {
        // Do it the .into way for variety and documentation
        let result: Endpoint = "namespace/component.endpoint".into();
        assert_eq!(result, vec!["namespace", "component", "endpoint"]);
258
259
260
    }

    #[test]
261
262
263
264
265
266
    fn test_empty_string() {
        let result = Endpoint::from("");
        assert_eq!(
            result,
            vec![DEFAULT_NAMESPACE, DEFAULT_COMPONENT, DEFAULT_ENDPOINT]
        );
267

268
269
270
271
272
        // White space is equivalent to an empty string
        let result = Endpoint::from("   ");
        assert_eq!(
            result,
            vec![DEFAULT_NAMESPACE, DEFAULT_COMPONENT, DEFAULT_ENDPOINT]
273
274
        );
    }
275
}