Skip to main content

zenoh_protocol/core/
region.rs

1//
2// Copyright (c) 2026 ZettaScale Technology
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7// which is available at https://www.apache.org/licenses/LICENSE-2.0.
8//
9// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10//
11// Contributors:
12//   ZettaScale Zenoh Team, <zenoh@zettascale.tech>
13//
14use alloc::string::{String, ToString};
15use core::{fmt::Display, str::FromStr};
16
17use serde::{de, Deserialize, Serialize};
18
19use crate::core::WhatAmI;
20
21/// Gateway bound.
22#[repr(u8)]
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub enum Bound {
25    #[default]
26    North,
27    South,
28}
29
30impl Bound {
31    pub fn is_north(&self) -> bool {
32        *self == Bound::North
33    }
34
35    pub fn is_south(&self) -> bool {
36        *self == Bound::South
37    }
38}
39
40impl Display for Bound {
41    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
42        match self {
43            Bound::North => f.write_str("north"),
44            Bound::South => f.write_str("south"),
45        }
46    }
47}
48
49impl TryFrom<u8> for Bound {
50    type Error = InvalidBoundError;
51
52    fn try_from(value: u8) -> Result<Self, Self::Error> {
53        match value {
54            v if v == Bound::North as u8 => Ok(Bound::North),
55            v if v == Bound::South as u8 => Ok(Bound::South),
56            _ => Err(InvalidBoundError),
57        }
58    }
59}
60
61#[derive(Debug)]
62pub struct InvalidBoundError;
63
64impl Display for InvalidBoundError {
65    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
66        write!(
67            f,
68            "a u8-encoded bound should either be {} (for '{}') or {} (for '{}')",
69            Bound::North as u8,
70            Bound::North,
71            Bound::South as u8,
72            Bound::South
73        )
74    }
75}
76
77#[cfg(feature = "std")]
78impl std::error::Error for InvalidBoundError {}
79
80/// Region identifier.
81#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
82pub enum Region {
83    /// Main region.
84    #[default]
85    North,
86    /// Subregion of local sessions.
87    Local,
88    /// User-defined subregion(s).
89    South { id: usize, mode: WhatAmI },
90}
91
92impl Region {
93    pub const fn default_south(mode: WhatAmI) -> Self {
94        Self::South { id: 0, mode }
95    }
96
97    pub fn bound(&self) -> Bound {
98        match self {
99            Region::North => Bound::North,
100            Region::South { .. } | Region::Local => Bound::South,
101        }
102    }
103
104    pub fn mode(&self) -> Option<WhatAmI> {
105        match self {
106            Region::North => None,
107            Region::Local => Some(WhatAmI::Client),
108            Region::South { mode, .. } => Some(*mode),
109        }
110    }
111}
112
113impl FromStr for Region {
114    type Err = InvalidRegionIdError;
115
116    fn from_str(s: &str) -> Result<Self, Self::Err> {
117        match s {
118            "north" => Ok(Region::North),
119            "local" => Ok(Region::Local),
120            _ => {
121                let mut substrings = s.splitn(3, ":");
122
123                let Some("south") = substrings.next() else {
124                    return Err(InvalidRegionIdError::ExpectedSouth);
125                };
126
127                let number_str = substrings
128                    .next()
129                    .ok_or(InvalidRegionIdError::ExpectedNumber)?;
130
131                let number = number_str
132                    .parse()
133                    .map_err(InvalidRegionIdError::BadNumber)?;
134
135                let mode_str = substrings
136                    .next()
137                    .ok_or(InvalidRegionIdError::ExpectedWhatAmI)?;
138
139                let mode = mode_str.parse().map_err(InvalidRegionIdError::BadWhatAmI)?;
140
141                debug_assert!(substrings.next().is_none());
142
143                Ok(Region::South { id: number, mode })
144            }
145        }
146    }
147}
148
149#[derive(Debug)]
150pub enum InvalidRegionIdError {
151    ExpectedSouth,
152    ExpectedNumber,
153    ExpectedWhatAmI,
154    ExpectedEof,
155    BadNumber(core::num::ParseIntError),
156    BadWhatAmI(zenoh_result::ZError),
157}
158
159impl Display for InvalidRegionIdError {
160    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
161        match self {
162            InvalidRegionIdError::ExpectedSouth => f.write_str("expected 'south' literal"),
163            InvalidRegionIdError::ExpectedNumber => f.write_str("expected u16 number"),
164            InvalidRegionIdError::ExpectedWhatAmI => f.write_str("expected mode"),
165            InvalidRegionIdError::ExpectedEof => f.write_str("expected EOF"),
166            InvalidRegionIdError::BadNumber(err) => write!(f, "error parsing number: {err}"),
167            InvalidRegionIdError::BadWhatAmI(err) => write!(f, "error parsing mode: {err}"),
168        }
169    }
170}
171
172impl Display for Region {
173    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
174        match self {
175            Region::North => f.write_str("north"),
176            Region::Local => f.write_str("local"),
177            Region::South { id, mode } => write!(f, "south:{id}:{mode}"),
178        }
179    }
180}
181
182impl Serialize for Region {
183    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
184    where
185        S: serde::Serializer,
186    {
187        serializer.collect_str(self)
188    }
189}
190
191impl<'de> Deserialize<'de> for Region {
192    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
193    where
194        D: serde::Deserializer<'de>,
195    {
196        String::deserialize(deserializer)?
197            .parse()
198            .map_err(de::Error::custom)
199    }
200}
201
202/// Region name.
203///
204/// A region name is a non-empty UTF-8 string limited to [`Self::MAX_LEN`] bytes. It is used to
205/// communicate (north) regions names in establishment as well as to match against said names in
206/// `gateway` configuration.
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct RegionName(String);
209
210impl RegionName {
211    pub const MAX_LEN: usize = 32;
212
213    pub fn as_str(&self) -> &str {
214        &self.0
215    }
216
217    pub fn into_string(self) -> String {
218        self.0
219    }
220
221    fn validate<S>(s: S) -> Result<S, InvalidRegionNameError>
222    where
223        S: AsRef<str>,
224    {
225        if s.as_ref().is_empty() {
226            return Err(InvalidRegionNameError::Empty);
227        }
228
229        if s.as_ref().len() > Self::MAX_LEN {
230            return Err(InvalidRegionNameError::TooLong);
231        }
232
233        Ok(s)
234    }
235
236    #[cfg(feature = "test")]
237    #[doc(hidden)]
238    pub fn rand() -> Self {
239        use rand::distributions::{Alphanumeric, DistString};
240
241        Alphanumeric
242            .sample_string(&mut rand::thread_rng(), Self::MAX_LEN)
243            .try_into()
244            .unwrap()
245    }
246}
247
248impl FromStr for RegionName {
249    type Err = InvalidRegionNameError;
250
251    fn from_str(s: &str) -> Result<Self, Self::Err> {
252        Self::validate(s).map(|s| Self(s.to_string()))
253    }
254}
255
256impl TryFrom<String> for RegionName {
257    type Error = InvalidRegionNameError;
258
259    fn try_from(s: String) -> Result<Self, Self::Error> {
260        Self::validate(s).map(Self)
261    }
262}
263
264#[derive(Debug)]
265pub enum InvalidRegionNameError {
266    Empty,
267    TooLong,
268}
269
270impl Display for InvalidRegionNameError {
271    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
272        match self {
273            InvalidRegionNameError::Empty => f.write_str("region names should be non-empty"),
274            InvalidRegionNameError::TooLong => write!(
275                f,
276                "region names should be at most {} bytes",
277                RegionName::MAX_LEN
278            ),
279        }
280    }
281}
282
283#[cfg(feature = "std")]
284impl std::error::Error for InvalidRegionNameError {}
285
286impl Serialize for RegionName {
287    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
288    where
289        S: serde::Serializer,
290    {
291        serializer.serialize_str(self.as_str())
292    }
293}
294
295impl<'de> Deserialize<'de> for RegionName {
296    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
297    where
298        D: serde::Deserializer<'de>,
299    {
300        String::deserialize(deserializer)?
301            .parse()
302            .map_err(de::Error::custom)
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use std::str::FromStr;
309
310    use crate::core::{Region, WhatAmI};
311
312    #[test]
313    fn test_region_parsing() {
314        assert!(Region::from_str("north:0:router").is_err());
315        assert!(Region::from_str("south").is_err());
316        assert!(Region::from_str("south:42").is_err());
317        assert!(Region::from_str("south:42:broker").is_err());
318        assert!(Region::from_str("south:0.1:router").is_err());
319        assert!(Region::from_str("south:3:router:???").is_err());
320
321        assert_eq!(Region::from_str("north").unwrap(), Region::North);
322        assert_eq!(Region::from_str("local").unwrap(), Region::Local);
323        assert_eq!(
324            Region::from_str("south:2000:peer").unwrap(),
325            Region::South {
326                id: 2000,
327                mode: WhatAmI::Peer
328            }
329        );
330    }
331
332    #[test]
333    fn test_region_formatting() {
334        assert_eq!(&format!("{}", Region::North), "north");
335        assert_eq!(&format!("{}", Region::Local), "local");
336        assert_eq!(
337            &format!(
338                "{}",
339                Region::South {
340                    id: 1,
341                    mode: WhatAmI::Client
342                }
343            ),
344            "south:1:client"
345        );
346        assert_eq!(
347            &format!(
348                "{}",
349                Region::South {
350                    id: 2,
351                    mode: WhatAmI::Peer
352                }
353            ),
354            "south:2:peer"
355        );
356        assert_eq!(
357            &format!(
358                "{}",
359                Region::South {
360                    id: 3,
361                    mode: WhatAmI::Router
362                }
363            ),
364            "south:3:router"
365        );
366    }
367}