zenoh_plugin_ros2dds/
config.rs

1//
2// Copyright (c) 2022 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 std::{env, fmt, time::Duration};
15
16use regex::Regex;
17use serde::{de, de::Visitor, ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer};
18use tracing::warn;
19use zenoh::{key_expr::OwnedKeyExpr, qos::Priority};
20
21pub const DEFAULT_NAMESPACE: &str = "/";
22pub const DEFAULT_NODENAME: &str = "zenoh_bridge_ros2dds";
23pub const DEFAULT_DOMAIN: u32 = 0;
24pub const DEFAULT_RELIABLE_ROUTES_BLOCKING: bool = true;
25pub const DEFAULT_TRANSIENT_LOCAL_CACHE_MULTIPLIER: usize = 10;
26pub const DEFAULT_DDS_LOCALHOST_ONLY: bool = false;
27pub const DEFAULT_QUERIES_TIMEOUT: f32 = 5.0;
28// In the ROS 2 action, get_result is sent out first and then wait for the result.
29// It will cause the action client never complete, so we need a larger timeout.
30// Refer to https://github.com/eclipse-zenoh/zenoh-plugin-ros2dds/issues/369#issuecomment-2563725619
31pub const DEFAULT_ACTION_GET_RESULT_TIMEOUT: f32 = 300.0;
32pub const DEFAULT_WORK_THREAD_NUM: usize = 2;
33pub const DEFAULT_MAX_BLOCK_THREAD_NUM: usize = 50;
34
35#[derive(Deserialize, Debug, Serialize)]
36#[serde(deny_unknown_fields)]
37pub struct Config {
38    #[serde(default = "default_namespace")]
39    pub namespace: String,
40    #[serde(default = "default_nodename")]
41    pub nodename: OwnedKeyExpr,
42    #[serde(default = "default_domain")]
43    pub domain: u32,
44    #[serde(default = "default_localhost_only")]
45    pub ros_localhost_only: bool,
46    #[serde(
47        default = "default_automatic_discovery_range",
48        deserialize_with = "deserialize_automatic_discovery_range"
49    )]
50    pub ros_automatic_discovery_range: Option<RosAutomaticDiscoveryRange>,
51    #[serde(default, deserialize_with = "deserialize_static_peers")]
52    pub ros_static_peers: Option<Vec<String>>,
53    #[serde(default, flatten)]
54    pub allowance: Option<Allowance>,
55    #[serde(
56        default,
57        deserialize_with = "deserialize_vec_regex_f32",
58        serialize_with = "serialize_vec_regex_f32"
59    )]
60    pub pub_max_frequencies: Vec<(Regex, f32)>,
61    #[serde(default)]
62    #[cfg(feature = "dds_shm")]
63    pub shm_enabled: bool,
64    #[serde(default = "default_transient_local_cache_multiplier")]
65    pub transient_local_cache_multiplier: usize,
66    #[serde(default = "default_queries_timeout")]
67    pub queries_timeout: Option<QueriesTimeouts>,
68    #[serde(default = "default_reliable_routes_blocking")]
69    pub reliable_routes_blocking: bool,
70    #[serde(
71        default,
72        deserialize_with = "deserialize_vec_regex_prio",
73        serialize_with = "serialize_vec_regex_prio"
74    )]
75    pub pub_priorities: Vec<(Regex, (Priority, bool))>,
76    #[serde(default = "default_work_thread_num")]
77    pub work_thread_num: usize,
78    #[serde(default = "default_max_block_thread_num")]
79    pub max_block_thread_num: usize,
80    __required__: Option<bool>,
81    #[serde(default, deserialize_with = "deserialize_path")]
82    __path__: Option<Vec<String>>,
83}
84
85impl Config {
86    pub fn get_pub_max_frequencies(&self, ros2_name: &str) -> Option<f32> {
87        for (re, freq) in &self.pub_max_frequencies {
88            if re.is_match(ros2_name) {
89                return Some(*freq);
90            }
91        }
92        None
93    }
94
95    pub fn get_pub_priority_and_express(&self, ros2_name: &str) -> Option<(Priority, bool)> {
96        for (re, p) in &self.pub_priorities {
97            if re.is_match(ros2_name) {
98                return Some(*p);
99            }
100        }
101        None
102    }
103
104    pub fn get_queries_timeout_tl_sub(&self, ros2_name: &str) -> Duration {
105        if let Some(qt) = &self.queries_timeout {
106            for (re, secs) in &qt.transient_local_subscribers {
107                if re.is_match(ros2_name) {
108                    return Duration::from_secs_f32(*secs);
109                }
110            }
111            return Duration::from_secs_f32(qt.default);
112        }
113        Duration::from_secs_f32(DEFAULT_QUERIES_TIMEOUT)
114    }
115
116    pub fn get_queries_timeout_service(&self, ros2_name: &str) -> Duration {
117        if let Some(qt) = &self.queries_timeout {
118            for (re, secs) in &qt.services {
119                if re.is_match(ros2_name) {
120                    return Duration::from_secs_f32(*secs);
121                }
122            }
123            return Duration::from_secs_f32(qt.default);
124        }
125        Duration::from_secs_f32(DEFAULT_QUERIES_TIMEOUT)
126    }
127
128    pub fn get_queries_timeout_action_send_goal(&self, ros2_name: &str) -> Duration {
129        match &self.queries_timeout {
130            Some(QueriesTimeouts {
131                default,
132                actions: Some(at),
133                ..
134            }) => {
135                for (re, secs) in &at.send_goal {
136                    if re.is_match(ros2_name) {
137                        return Duration::from_secs_f32(*secs);
138                    }
139                }
140                Duration::from_secs_f32(*default)
141            }
142            Some(QueriesTimeouts {
143                default,
144                actions: None,
145                ..
146            }) => Duration::from_secs_f32(*default),
147            _ => Duration::from_secs_f32(DEFAULT_QUERIES_TIMEOUT),
148        }
149    }
150
151    pub fn get_queries_timeout_action_cancel_goal(&self, ros2_name: &str) -> Duration {
152        match &self.queries_timeout {
153            Some(QueriesTimeouts {
154                default,
155                actions: Some(at),
156                ..
157            }) => {
158                for (re, secs) in &at.cancel_goal {
159                    if re.is_match(ros2_name) {
160                        return Duration::from_secs_f32(*secs);
161                    }
162                }
163                Duration::from_secs_f32(*default)
164            }
165            Some(QueriesTimeouts {
166                default,
167                actions: None,
168                ..
169            }) => Duration::from_secs_f32(*default),
170            _ => Duration::from_secs_f32(DEFAULT_QUERIES_TIMEOUT),
171        }
172    }
173
174    pub fn get_queries_timeout_action_get_result(&self, ros2_name: &str) -> Duration {
175        match &self.queries_timeout {
176            Some(QueriesTimeouts {
177                actions: Some(at), ..
178            }) => {
179                for (re, secs) in &at.get_result {
180                    if re.is_match(ros2_name) {
181                        return Duration::from_secs_f32(*secs);
182                    }
183                }
184                Duration::from_secs_f32(DEFAULT_ACTION_GET_RESULT_TIMEOUT)
185            }
186            _ => Duration::from_secs_f32(DEFAULT_ACTION_GET_RESULT_TIMEOUT),
187        }
188    }
189}
190
191#[derive(Deserialize, Debug, Serialize)]
192#[serde(deny_unknown_fields)]
193pub struct QueriesTimeouts {
194    #[serde(default = "default_queries_timeout_default")]
195    default: f32,
196    #[serde(
197        default,
198        deserialize_with = "deserialize_vec_regex_f32",
199        serialize_with = "serialize_vec_regex_f32"
200    )]
201    transient_local_subscribers: Vec<(Regex, f32)>,
202    #[serde(
203        default,
204        deserialize_with = "deserialize_vec_regex_f32",
205        serialize_with = "serialize_vec_regex_f32"
206    )]
207    services: Vec<(Regex, f32)>,
208    #[serde(default = "default_actions_timeout")]
209    actions: Option<ActionsTimeouts>,
210}
211
212#[derive(Deserialize, Debug, Serialize)]
213#[serde(deny_unknown_fields)]
214pub struct ActionsTimeouts {
215    #[serde(
216        default,
217        deserialize_with = "deserialize_vec_regex_f32",
218        serialize_with = "serialize_vec_regex_f32"
219    )]
220    send_goal: Vec<(Regex, f32)>,
221    #[serde(
222        default,
223        deserialize_with = "deserialize_vec_regex_f32",
224        serialize_with = "serialize_vec_regex_f32"
225    )]
226    cancel_goal: Vec<(Regex, f32)>,
227    #[serde(
228        default = "default_actions_get_result_timeout",
229        deserialize_with = "deserialize_vec_regex_f32",
230        serialize_with = "serialize_vec_regex_f32"
231    )]
232    get_result: Vec<(Regex, f32)>,
233}
234
235#[derive(Deserialize, Debug, Serialize)]
236pub enum Allowance {
237    #[serde(rename = "allow")]
238    Allow(ROS2InterfacesRegex),
239    #[serde(rename = "deny")]
240    Deny(ROS2InterfacesRegex),
241}
242
243impl Allowance {
244    pub fn is_publisher_allowed(&self, name: &str) -> bool {
245        use Allowance::*;
246        match self {
247            Allow(r) => r
248                .publishers
249                .as_ref()
250                .map(|re| re.is_match(name))
251                .unwrap_or(false),
252            Deny(r) => r
253                .publishers
254                .as_ref()
255                .map(|re| !re.is_match(name))
256                .unwrap_or(true),
257        }
258    }
259
260    pub fn is_subscriber_allowed(&self, name: &str) -> bool {
261        use Allowance::*;
262        match self {
263            Allow(r) => r
264                .subscribers
265                .as_ref()
266                .map(|re| re.is_match(name))
267                .unwrap_or(false),
268            Deny(r) => r
269                .subscribers
270                .as_ref()
271                .map(|re| !re.is_match(name))
272                .unwrap_or(true),
273        }
274    }
275
276    pub fn is_service_srv_allowed(&self, name: &str) -> bool {
277        use Allowance::*;
278        match self {
279            Allow(r) => r
280                .service_servers
281                .as_ref()
282                .map(|re| re.is_match(name))
283                .unwrap_or(false),
284            Deny(r) => r
285                .service_servers
286                .as_ref()
287                .map(|re| !re.is_match(name))
288                .unwrap_or(true),
289        }
290    }
291
292    pub fn is_service_cli_allowed(&self, name: &str) -> bool {
293        use Allowance::*;
294        match self {
295            Allow(r) => r
296                .service_clients
297                .as_ref()
298                .map(|re| re.is_match(name))
299                .unwrap_or(false),
300            Deny(r) => r
301                .service_clients
302                .as_ref()
303                .map(|re| !re.is_match(name))
304                .unwrap_or(true),
305        }
306    }
307
308    pub fn is_action_srv_allowed(&self, name: &str) -> bool {
309        use Allowance::*;
310        match self {
311            Allow(r) => r
312                .action_servers
313                .as_ref()
314                .map(|re| re.is_match(name))
315                .unwrap_or(false),
316            Deny(r) => r
317                .action_servers
318                .as_ref()
319                .map(|re| !re.is_match(name))
320                .unwrap_or(true),
321        }
322    }
323
324    pub fn is_action_cli_allowed(&self, name: &str) -> bool {
325        use Allowance::*;
326        match self {
327            Allow(r) => r
328                .action_clients
329                .as_ref()
330                .map(|re| re.is_match(name))
331                .unwrap_or(false),
332            Deny(r) => r
333                .action_clients
334                .as_ref()
335                .map(|re| !re.is_match(name))
336                .unwrap_or(true),
337        }
338    }
339}
340
341#[derive(Deserialize, Debug, Default, Serialize)]
342pub struct ROS2InterfacesRegex {
343    #[serde(
344        default,
345        deserialize_with = "deserialize_regex",
346        serialize_with = "serialize_regex",
347        skip_serializing_if = "Option::is_none"
348    )]
349    pub publishers: Option<Regex>,
350    #[serde(
351        default,
352        deserialize_with = "deserialize_regex",
353        serialize_with = "serialize_regex",
354        skip_serializing_if = "Option::is_none"
355    )]
356    pub subscribers: Option<Regex>,
357    #[serde(
358        default,
359        deserialize_with = "deserialize_regex",
360        serialize_with = "serialize_regex",
361        skip_serializing_if = "Option::is_none"
362    )]
363    pub service_servers: Option<Regex>,
364    #[serde(
365        default,
366        deserialize_with = "deserialize_regex",
367        serialize_with = "serialize_regex",
368        skip_serializing_if = "Option::is_none"
369    )]
370    pub service_clients: Option<Regex>,
371    #[serde(
372        default,
373        deserialize_with = "deserialize_regex",
374        serialize_with = "serialize_regex",
375        skip_serializing_if = "Option::is_none"
376    )]
377    pub action_servers: Option<Regex>,
378    #[serde(
379        default,
380        deserialize_with = "deserialize_regex",
381        serialize_with = "serialize_regex",
382        skip_serializing_if = "Option::is_none"
383    )]
384    pub action_clients: Option<Regex>,
385}
386
387fn default_namespace() -> String {
388    DEFAULT_NAMESPACE.to_string()
389}
390
391fn default_nodename() -> OwnedKeyExpr {
392    unsafe { OwnedKeyExpr::from_string_unchecked(DEFAULT_NODENAME.into()) }
393}
394
395fn default_domain() -> u32 {
396    if let Ok(s) = env::var("ROS_DOMAIN_ID") {
397        s.parse::<u32>().unwrap_or(DEFAULT_DOMAIN)
398    } else {
399        DEFAULT_DOMAIN
400    }
401}
402
403fn default_queries_timeout() -> Option<QueriesTimeouts> {
404    Some(QueriesTimeouts {
405        default: default_queries_timeout_default(),
406        transient_local_subscribers: Vec::new(),
407        services: Vec::new(),
408        actions: default_actions_timeout(),
409    })
410}
411
412fn default_queries_timeout_default() -> f32 {
413    DEFAULT_QUERIES_TIMEOUT
414}
415
416fn default_actions_timeout() -> Option<ActionsTimeouts> {
417    Some(ActionsTimeouts {
418        send_goal: Vec::new(),
419        cancel_goal: Vec::new(),
420        get_result: default_actions_get_result_timeout(),
421    })
422}
423
424fn default_actions_get_result_timeout() -> Vec<(Regex, f32)> {
425    vec![(Regex::new(".*").unwrap(), DEFAULT_ACTION_GET_RESULT_TIMEOUT)]
426}
427
428fn deserialize_path<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
429where
430    D: Deserializer<'de>,
431{
432    deserializer.deserialize_option(OptPathVisitor)
433}
434
435struct OptPathVisitor;
436
437impl<'de> serde::de::Visitor<'de> for OptPathVisitor {
438    type Value = Option<Vec<String>>;
439
440    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
441        write!(formatter, "none or a string or an array of strings")
442    }
443
444    fn visit_none<E>(self) -> Result<Self::Value, E>
445    where
446        E: de::Error,
447    {
448        Ok(None)
449    }
450
451    fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
452    where
453        D: Deserializer<'de>,
454    {
455        deserializer.deserialize_any(PathVisitor).map(Some)
456    }
457}
458
459struct PathVisitor;
460
461impl<'de> serde::de::Visitor<'de> for PathVisitor {
462    type Value = Vec<String>;
463
464    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
465        write!(formatter, "a string or an array of strings")
466    }
467
468    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
469    where
470        E: de::Error,
471    {
472        Ok(vec![v.into()])
473    }
474
475    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
476    where
477        A: de::SeqAccess<'de>,
478    {
479        let mut v = if let Some(l) = seq.size_hint() {
480            Vec::with_capacity(l)
481        } else {
482            Vec::new()
483        };
484        while let Some(s) = seq.next_element()? {
485            v.push(s);
486        }
487        Ok(v)
488    }
489}
490
491fn default_reliable_routes_blocking() -> bool {
492    DEFAULT_RELIABLE_ROUTES_BLOCKING
493}
494
495#[derive(Deserialize, Debug, Serialize, Eq, PartialEq, Clone, Copy)]
496pub enum RosAutomaticDiscoveryRange {
497    Subnet,
498    Localhost,
499    Off,
500    SystemDefault,
501}
502
503fn default_localhost_only() -> bool {
504    env::var("ROS_LOCALHOST_ONLY").as_deref() == Ok("1")
505}
506
507fn default_automatic_discovery_range() -> Option<RosAutomaticDiscoveryRange> {
508    match env::var("ROS_AUTOMATIC_DISCOVERY_RANGE").as_deref() {
509        Ok("SUBNET") => Some(RosAutomaticDiscoveryRange::Subnet),
510        Ok("LOCALHOST") => Some(RosAutomaticDiscoveryRange::Localhost),
511        Ok("OFF") => Some(RosAutomaticDiscoveryRange::Localhost),
512        Ok("SYSTEM_DEFAULT") => Some(RosAutomaticDiscoveryRange::SystemDefault),
513        Ok(value) => {
514            warn!(
515                r#"Invalid value for environment variable ROS_AUTOMATIC_DISCOVERY_RANGE ("{value}"). Using "SUBNET" instead "#
516            );
517            Some(RosAutomaticDiscoveryRange::Subnet)
518        }
519        Err(_) => None,
520    }
521}
522
523fn deserialize_automatic_discovery_range<'de, D>(
524    deserializer: D,
525) -> Result<Option<RosAutomaticDiscoveryRange>, D::Error>
526where
527    D: Deserializer<'de>,
528{
529    let discovery_range: String = Deserialize::deserialize(deserializer).unwrap();
530    match discovery_range.as_str() {
531        "SUBNET" => Ok(Some(RosAutomaticDiscoveryRange::Subnet)),
532        "LOCALHOST" => Ok(Some(RosAutomaticDiscoveryRange::Localhost)),
533        "OFF" => Ok(Some(RosAutomaticDiscoveryRange::Off)),
534        "SYSTEM_DEFAULT" => Ok(Some(RosAutomaticDiscoveryRange::SystemDefault)),
535        unknown => Err(de::Error::custom(format!(
536            r#"Invalid parameter "{unknown}" for ROS_AUTOMATICALLY_DISCOVERY_RANGE"#
537        ))),
538    }
539}
540
541fn deserialize_static_peers<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
542where
543    D: Deserializer<'de>,
544{
545    let peers: String = Deserialize::deserialize(deserializer).unwrap();
546    let mut peer_list: Vec<String> = Vec::new();
547    for peer in peers.split(';') {
548        if !peer.is_empty() {
549            peer_list.push(peer.to_owned());
550        }
551    }
552    if peer_list.is_empty() {
553        Ok(None)
554    } else {
555        Ok(Some(peer_list))
556    }
557}
558
559fn default_transient_local_cache_multiplier() -> usize {
560    DEFAULT_TRANSIENT_LOCAL_CACHE_MULTIPLIER
561}
562
563fn default_work_thread_num() -> usize {
564    DEFAULT_WORK_THREAD_NUM
565}
566
567fn default_max_block_thread_num() -> usize {
568    DEFAULT_MAX_BLOCK_THREAD_NUM
569}
570
571fn serialize_regex<S>(r: &Option<Regex>, serializer: S) -> Result<S::Ok, S::Error>
572where
573    S: Serializer,
574{
575    match r {
576        Some(ex) => serializer.serialize_some(ex.as_str()),
577        None => serializer.serialize_none(),
578    }
579}
580
581fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error>
582where
583    D: Deserializer<'de>,
584{
585    deserializer.deserialize_any(RegexVisitor)
586}
587
588// Serde Visitor for Regex deserialization.
589// It accepts either a String, either a list of Strings (that are concatenated with `|`)
590struct RegexVisitor;
591
592impl<'de> Visitor<'de> for RegexVisitor {
593    type Value = Option<Regex>;
594
595    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
596        formatter.write_str(r#"either a string or a list of strings"#)
597    }
598
599    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
600    where
601        E: de::Error,
602    {
603        Regex::new(&format!("^{value}$"))
604            .map(Some)
605            .map_err(|e| de::Error::custom(format!("Invalid regex '{value}': {e}")))
606    }
607
608    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
609    where
610        A: de::SeqAccess<'de>,
611    {
612        let mut vec: Vec<String> = Vec::new();
613        while let Some(s) = seq.next_element::<String>()? {
614            vec.push(format!("^{s}$"));
615        }
616        if vec.is_empty() {
617            return Ok(None);
618        };
619
620        let s: String = vec.join("|");
621        Regex::new(&s)
622            .map(Some)
623            .map_err(|e| de::Error::custom(format!("Invalid regex '{s}': {e}")))
624    }
625}
626
627fn deserialize_vec_regex_f32<'de, D>(deserializer: D) -> Result<Vec<(Regex, f32)>, D::Error>
628where
629    D: Deserializer<'de>,
630{
631    #[derive(Deserialize)]
632    #[serde(untagged)]
633    enum AcceptedValues {
634        Float(f32),
635        List(Vec<String>),
636    }
637
638    let values: AcceptedValues = Deserialize::deserialize(deserializer).unwrap();
639    match values {
640        AcceptedValues::Float(f) => {
641            // same float for any string (i.e. matching ".*")
642            Ok(vec![(Regex::new(".*").unwrap(), f)])
643        }
644        AcceptedValues::List(strs) => {
645            let mut result: Vec<(Regex, f32)> = Vec::with_capacity(strs.len());
646            for s in strs {
647                let i = s.find('=').ok_or_else(|| {
648                    de::Error::custom(format!(
649                        r#"Invalid list of "<regex>=<float>" elements": {s}"#
650                    ))
651                })?;
652                let regex = Regex::new(&s[0..i])
653                    .map_err(|e| de::Error::custom(format!("Invalid regex in '{s}': {e}")))?;
654                let frequency: f32 = s[i + 1..]
655                    .parse()
656                    .map_err(|e| de::Error::custom(format!("Invalid float value in '{s}': {e}")))?;
657                result.push((regex, frequency));
658            }
659            Ok(result)
660        }
661    }
662}
663
664fn serialize_vec_regex_f32<S>(v: &Vec<(Regex, f32)>, serializer: S) -> Result<S::Ok, S::Error>
665where
666    S: Serializer,
667{
668    let mut seq = serializer.serialize_seq(Some(v.len()))?;
669    for (r, f) in v {
670        let s = format!("{}={}", r.as_str(), f);
671        seq.serialize_element(&s)?;
672    }
673    seq.end()
674}
675
676#[allow(clippy::type_complexity)]
677fn deserialize_vec_regex_prio<'de, D>(
678    deserializer: D,
679) -> Result<Vec<(Regex, (Priority, bool))>, D::Error>
680where
681    D: Deserializer<'de>,
682{
683    let strs: Vec<String> = Deserialize::deserialize(deserializer).unwrap();
684    let mut result: Vec<(Regex, (Priority, bool))> = Vec::with_capacity(strs.len());
685    for s in strs {
686        let i = s.find('=').ok_or_else(|| {
687            de::Error::custom(format!(
688                r#"Invalid list of "<regex>=<int>[:express]" elements": {s}"#
689            ))
690        })?;
691        let regex = Regex::new(&s[0..i])
692            .map_err(|e| de::Error::custom(format!("Invalid regex in '{s}': {e}")))?;
693        let (prio_str, is_express) = match s[i + 1..].strip_suffix(":express") {
694            Some(prio_str) => (prio_str, true),
695            None => (&s[i + 1..], false),
696        };
697        let i: u8 = prio_str.parse().map_err(|e| {
698            de::Error::custom(format!(
699                "Invalid priority (format is not <int>[:express]) in '{s}': {e}"
700            ))
701        })?;
702        let priority = Priority::try_from(i)
703            .map_err(|e| de::Error::custom(format!("Invalid priority in '{s}': {e}")))?;
704        result.push((regex, (priority, is_express)));
705    }
706    Ok(result)
707}
708
709fn serialize_vec_regex_prio<S>(
710    v: &Vec<(Regex, (Priority, bool))>,
711    serializer: S,
712) -> Result<S::Ok, S::Error>
713where
714    S: Serializer,
715{
716    let mut seq = serializer.serialize_seq(Some(v.len()))?;
717    for (r, (p, is_express)) in v {
718        let s = if *is_express {
719            format!("{}={}:express", r.as_str(), *p as u8)
720        } else {
721            format!("{}={}", r.as_str(), *p as u8)
722        };
723        seq.serialize_element(&s)?;
724    }
725    seq.end()
726}
727
728pub fn serialize_duration_as_f32<S>(d: &Duration, serializer: S) -> Result<S::Ok, S::Error>
729where
730    S: Serializer,
731{
732    serializer.serialize_f32(d.as_secs_f32())
733}
734
735#[cfg(test)]
736mod tests {
737    use test_case::test_case;
738
739    use super::{Config, RosAutomaticDiscoveryRange};
740
741    #[test]
742    fn test_allowance() {
743        use super::*;
744
745        let allow: Allowance = serde_json::from_str(
746            r#"{
747                "allow": {
748                    "publishers": ["/tf", ".*/pose"],
749                    "subscribers": [],
750                    "service_servers": [".*"],
751                    "action_servers": [".*/rotate_absolute"],
752                    "action_clients": [ "" ]
753                }
754            }"#,
755        )
756        .unwrap();
757        println!("allow: {}", serde_json::to_string(&allow).unwrap());
758
759        assert!(matches!(
760            allow,
761            Allowance::Allow(ROS2InterfacesRegex {
762                publishers: Some(_),
763                subscribers: None,
764                service_servers: Some(_),
765                service_clients: None,
766                action_servers: Some(_),
767                action_clients: Some(_),
768            })
769        ));
770
771        assert!(allow.is_publisher_allowed("/tf"));
772        assert!(allow.is_publisher_allowed("/x/y/pose"));
773        assert!(!allow.is_publisher_allowed("/abc/rotate_absolute"));
774        assert!(!allow.is_publisher_allowed("/cmd_vel"));
775        assert!(!allow.is_service_cli_allowed("/some_pseudo_random_name"));
776
777        assert!(!allow.is_subscriber_allowed("/tf"));
778        assert!(!allow.is_subscriber_allowed("/x/y/pose"));
779        assert!(!allow.is_publisher_allowed("/abc/rotate_absolute"));
780        assert!(!allow.is_subscriber_allowed("/cmd_vel"));
781        assert!(!allow.is_service_cli_allowed("/some_pseudo_random_name"));
782
783        assert!(allow.is_service_srv_allowed("/tf"));
784        assert!(allow.is_service_srv_allowed("/x/y/pose"));
785        assert!(allow.is_service_srv_allowed("/abc/rotate_absolute"));
786        assert!(allow.is_service_srv_allowed("/cmd_vel"));
787        assert!(allow.is_service_srv_allowed("/some_pseudo_random_name"));
788
789        assert!(!allow.is_service_cli_allowed("/tf"));
790        assert!(!allow.is_service_cli_allowed("/x/y/pose"));
791        assert!(!allow.is_service_cli_allowed("/abc/rotate_absolute"));
792        assert!(!allow.is_service_cli_allowed("/cmd_vel"));
793        assert!(!allow.is_service_cli_allowed("/some_pseudo_random_name"));
794
795        assert!(!allow.is_action_srv_allowed("/tf"));
796        assert!(!allow.is_action_srv_allowed("/x/y/pose"));
797        assert!(allow.is_action_srv_allowed("/abc/rotate_absolute"));
798        assert!(!allow.is_action_srv_allowed("/cmd_vel"));
799        assert!(!allow.is_action_srv_allowed("/some_pseudo_random_name"));
800
801        assert!(!allow.is_action_cli_allowed("/tf"));
802        assert!(!allow.is_action_cli_allowed("/x/y/pose"));
803        assert!(!allow.is_action_cli_allowed("/abc/rotate_absolute"));
804        assert!(!allow.is_action_cli_allowed("/cmd_vel"));
805        assert!(!allow.is_action_cli_allowed("/some_pseudo_random_name"));
806
807        let deny: Allowance = serde_json::from_str(
808            r#"{
809                "deny": {
810                    "publishers": ["/tf", ".*/pose"],
811                    "subscribers": [],
812                    "service_servers": [".*"],
813                    "action_servers": [".*/rotate_absolute"],
814                    "action_clients": [ "" ]
815                }
816            }"#,
817        )
818        .unwrap();
819        println!("deny: {}", serde_json::to_string(&allow).unwrap());
820
821        assert!(matches!(
822            deny,
823            Allowance::Deny(ROS2InterfacesRegex {
824                publishers: Some(_),
825                subscribers: None,
826                service_servers: Some(_),
827                service_clients: None,
828                action_servers: Some(_),
829                action_clients: Some(_),
830            })
831        ));
832
833        assert!(!deny.is_publisher_allowed("/tf"));
834        assert!(!deny.is_publisher_allowed("/x/y/pose"));
835        assert!(deny.is_publisher_allowed("/abc/rotate_absolute"));
836        assert!(deny.is_publisher_allowed("/cmd_vel"));
837        assert!(deny.is_service_cli_allowed("/some_pseudo_random_name"));
838
839        assert!(deny.is_subscriber_allowed("/tf"));
840        assert!(deny.is_subscriber_allowed("/x/y/pose"));
841        assert!(deny.is_publisher_allowed("/abc/rotate_absolute"));
842        assert!(deny.is_subscriber_allowed("/cmd_vel"));
843        assert!(deny.is_service_cli_allowed("/some_pseudo_random_name"));
844
845        assert!(!deny.is_service_srv_allowed("/tf"));
846        assert!(!deny.is_service_srv_allowed("/x/y/pose"));
847        assert!(!deny.is_service_srv_allowed("/abc/rotate_absolute"));
848        assert!(!deny.is_service_srv_allowed("/cmd_vel"));
849        assert!(!deny.is_service_srv_allowed("/some_pseudo_random_name"));
850
851        assert!(deny.is_service_cli_allowed("/tf"));
852        assert!(deny.is_service_cli_allowed("/x/y/pose"));
853        assert!(deny.is_service_cli_allowed("/abc/rotate_absolute"));
854        assert!(deny.is_service_cli_allowed("/cmd_vel"));
855        assert!(deny.is_service_cli_allowed("/some_pseudo_random_name"));
856
857        assert!(deny.is_action_srv_allowed("/tf"));
858        assert!(deny.is_action_srv_allowed("/x/y/pose"));
859        assert!(!deny.is_action_srv_allowed("/abc/rotate_absolute"));
860        assert!(deny.is_action_srv_allowed("/cmd_vel"));
861        assert!(deny.is_action_srv_allowed("/some_pseudo_random_name"));
862
863        assert!(deny.is_action_cli_allowed("/tf"));
864        assert!(deny.is_action_cli_allowed("/x/y/pose"));
865        assert!(deny.is_action_cli_allowed("/abc/rotate_absolute"));
866        assert!(deny.is_action_cli_allowed("/cmd_vel"));
867        assert!(deny.is_action_cli_allowed("/some_pseudo_random_name"));
868
869        let invalid = serde_json::from_str::<Allowance>(
870            r#"{
871                "allow": {
872                    "publishers": ["/tf", ".*/pose"],
873                    "subscribers": [],
874                    "service_servers": [".*"],
875                    "action_servers": [".*/rotate_absolute"],
876                    "action_clients": [ "" ]
877                },
878                "deny": {
879                    "subscribers": ["/tf", ".*/pose"],
880                    "service_clients": [".*"],
881                    "action_servers": [""],
882                    "action_clients": [ ".*/rotate_absolute" ]
883                },
884            }"#,
885        );
886        assert!(invalid.is_err());
887    }
888
889    #[test]
890    fn test_path_field() {
891        // See: https://github.com/eclipse-zenoh/zenoh-plugin-webserver/issues/19
892        let config = serde_json::from_str::<Config>(r#"{"__path__": "/example/path"}"#);
893
894        assert!(config.is_ok());
895        let Config {
896            __required__,
897            __path__,
898            ..
899        } = config.unwrap();
900
901        assert_eq!(__path__, Some(vec![String::from("/example/path")]));
902        assert_eq!(__required__, None);
903    }
904
905    #[test]
906    fn test_required_field() {
907        // See: https://github.com/eclipse-zenoh/zenoh-plugin-webserver/issues/19
908        let config = serde_json::from_str::<Config>(r#"{"__required__": true}"#);
909        assert!(config.is_ok());
910        let Config {
911            __required__,
912            __path__,
913            ..
914        } = config.unwrap();
915
916        assert_eq!(__path__, None);
917        assert_eq!(__required__, Some(true));
918    }
919
920    #[test]
921    fn test_path_field_and_required_field() {
922        // See: https://github.com/eclipse-zenoh/zenoh-plugin-webserver/issues/19
923        let config = serde_json::from_str::<Config>(
924            r#"{"__path__": "/example/path", "__required__": true}"#,
925        );
926
927        assert!(config.is_ok());
928        let Config {
929            __required__,
930            __path__,
931            ..
932        } = config.unwrap();
933
934        assert_eq!(__path__, Some(vec![String::from("/example/path")]));
935        assert_eq!(__required__, Some(true));
936    }
937
938    #[test]
939    fn test_no_path_field_and_no_required_field() {
940        // See: https://github.com/eclipse-zenoh/zenoh-plugin-webserver/issues/19
941        let config = serde_json::from_str::<Config>("{}");
942
943        assert!(config.is_ok());
944        let Config {
945            __required__,
946            __path__,
947            ..
948        } = config.unwrap();
949
950        assert_eq!(__path__, None);
951        assert_eq!(__required__, None);
952    }
953
954    #[test_case("{}", None; "Empty tests")]
955    #[test_case(r#"{"ros_automatic_discovery_range": "SUBNET"}"#, Some(RosAutomaticDiscoveryRange::Subnet); "SUBNET tests")]
956    #[test_case(r#"{"ros_automatic_discovery_range": "LOCALHOST"}"#, Some(RosAutomaticDiscoveryRange::Localhost); "LOCALHOST tests")]
957    #[test_case(r#"{"ros_automatic_discovery_range": "OFF"}"#, Some(RosAutomaticDiscoveryRange::Off); "OFF tests")]
958    #[test_case(r#"{"ros_automatic_discovery_range": "SYSTEM_DEFAULT"}"#, Some(RosAutomaticDiscoveryRange::SystemDefault); "SYSTEM_DEFAULT tests")]
959    fn test_ros_automatic_discovery_range(
960        config: &str,
961        result: Option<RosAutomaticDiscoveryRange>,
962    ) {
963        // Avoid the current environmental variables affect the result
964        // In ROS 2 Jazzy environment, ROS_AUTOMATIC_DISCOVERY_RANGE will be set to SUBNET automatically
965        // This will cause the empty config test fail.
966        std::env::remove_var("ROS_AUTOMATIC_DISCOVERY_RANGE");
967        let config = serde_json::from_str::<Config>(config);
968        assert!(config.is_ok());
969        let Config {
970            ros_automatic_discovery_range,
971            ..
972        } = config.unwrap();
973        assert_eq!(ros_automatic_discovery_range, result);
974    }
975
976    #[test_case("{}", None; "Empty tests")]
977    #[test_case(r#"{"ros_static_peers": "127.0.0.1"}"#, Some(vec!["127.0.0.1".to_owned()]); "Single peer")]
978    #[test_case(r#"{"ros_static_peers": "192.168.1.1;192.168.1.2"}"#, Some(vec!["192.168.1.1".to_owned(), "192.168.1.2".to_owned()]); "Multiple peers")]
979    fn test_ros_static_peers(config: &str, result: Option<Vec<String>>) {
980        // Avoid the current environmental variables affect the result
981        std::env::remove_var("ROS_STATIC_PEERS");
982        let config = serde_json::from_str::<Config>(config);
983        assert!(config.is_ok());
984        let Config {
985            ros_static_peers, ..
986        } = config.unwrap();
987        assert_eq!(ros_static_peers, result);
988    }
989}