Skip to main content

zerodds_grpc_bridge/
path.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! gRPC HTTP `:path` Parsing — Spec §"Call-Definition".
5
6use alloc::string::String;
7use core::fmt;
8
9/// Path-Parsing-Fehler.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum PathError {
12    /// Path startet nicht mit `/`.
13    NotAbsolute,
14    /// Path hat nicht das Format `/<service>/<method>`.
15    MalformedPath,
16}
17
18impl fmt::Display for PathError {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Self::NotAbsolute => f.write_str("path must start with /"),
22            Self::MalformedPath => f.write_str("path must be /<service>/<method>"),
23        }
24    }
25}
26
27#[cfg(feature = "std")]
28impl std::error::Error for PathError {}
29
30/// Spec §"Path" — `:path` "/" Service-Name "/" {method-name}.
31///
32/// Liefert `(service_name, method_name)`. Beide MUST non-empty sein.
33///
34/// # Errors
35/// Siehe [`PathError`].
36pub fn parse_path(path: &str) -> Result<(String, String), PathError> {
37    let stripped = path.strip_prefix('/').ok_or(PathError::NotAbsolute)?;
38    let mut parts = stripped.splitn(2, '/');
39    let service = parts.next().ok_or(PathError::MalformedPath)?;
40    let method = parts.next().ok_or(PathError::MalformedPath)?;
41    if service.is_empty() || method.is_empty() {
42        return Err(PathError::MalformedPath);
43    }
44    if method.contains('/') {
45        // Spec — method is "{ method name }" (single segment).
46        return Err(PathError::MalformedPath);
47    }
48    Ok((String::from(service), String::from(method)))
49}
50
51#[cfg(test)]
52#[allow(clippy::expect_used)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn parses_standard_grpc_path() {
58        // Spec §"Path" Beispiel.
59        let (s, m) = parse_path("/helloworld.Greeter/SayHello").expect("ok");
60        assert_eq!(s, "helloworld.Greeter");
61        assert_eq!(m, "SayHello");
62    }
63
64    #[test]
65    fn rejects_relative_path() {
66        assert_eq!(
67            parse_path("helloworld.Greeter/SayHello"),
68            Err(PathError::NotAbsolute)
69        );
70    }
71
72    #[test]
73    fn rejects_path_without_method() {
74        assert_eq!(
75            parse_path("/helloworld.Greeter"),
76            Err(PathError::MalformedPath)
77        );
78    }
79
80    #[test]
81    fn rejects_empty_service() {
82        assert_eq!(parse_path("//SayHello"), Err(PathError::MalformedPath));
83    }
84
85    #[test]
86    fn rejects_empty_method() {
87        assert_eq!(parse_path("/Service/"), Err(PathError::MalformedPath));
88    }
89
90    #[test]
91    fn rejects_method_with_slash() {
92        // Spec — method ist single segment.
93        assert_eq!(
94            parse_path("/Service/Method/Sub"),
95            Err(PathError::MalformedPath)
96        );
97    }
98
99    #[test]
100    fn parses_dotted_service_name() {
101        // Spec — Service-Name kann fully-qualified sein.
102        let (s, m) = parse_path("/google.api.something.LongServiceName/MyMethod").expect("ok");
103        assert_eq!(s, "google.api.something.LongServiceName");
104        assert_eq!(m, "MyMethod");
105    }
106}