Skip to main content

nros_params/
typed.rs

1//! Typed parameter API
2//!
3//! This module provides a fluent builder pattern for declaring typed parameters
4//! in a ROS 2 node, aligning with the rclrs API.
5
6#[cfg(test)]
7use crate::ParameterStorage;
8use crate::{
9    NodeKey, ParameterDescriptor, ParameterRange, ParameterServer, ParameterType,
10    SetParameterResult,
11};
12use heapless::String;
13
14/// Trait for types that can be used as typed parameters
15pub use crate::ParameterVariant;
16
17/// Error type for typed parameter operations
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ParameterError {
20    /// Parameter already declared with a different type
21    TypeMismatch,
22    /// Value is outside allowed range
23    OutOfRange,
24    /// Parameter is read-only
25    ReadOnly,
26    /// Parameter not found
27    NotFound,
28    /// Internal storage is full
29    StorageFull,
30    /// String conversion failed
31    StringConversion,
32    /// Invalid range for type
33    InvalidRange,
34    /// An on-set-parameters callback refused the write (phase-417 W4.a).
35    Rejected,
36}
37
38impl From<SetParameterResult> for ParameterError {
39    fn from(result: SetParameterResult) -> Self {
40        match result {
41            SetParameterResult::TypeMismatch => ParameterError::TypeMismatch,
42            SetParameterResult::OutOfRange => ParameterError::OutOfRange,
43            SetParameterResult::ReadOnly => ParameterError::ReadOnly,
44            SetParameterResult::NotFound => ParameterError::NotFound,
45            SetParameterResult::StorageFull => ParameterError::StorageFull,
46            SetParameterResult::InvalidRange => ParameterError::InvalidRange,
47            SetParameterResult::Rejected => ParameterError::Rejected,
48            // The typed API never auto-declares, so an unknown name is
49            // simply not there.
50            SetParameterResult::Undeclared => ParameterError::NotFound,
51            SetParameterResult::Success => {
52                panic!("SetParameterResult::Success is not an error")
53            }
54        }
55    }
56}
57
58/// Trait for types that can be converted to a parameter range from `RangeInclusive`
59pub trait RangeConvertible: ParameterVariant + Clone {
60    /// Convert a `RangeInclusive<Self>` to a `ParameterRange`
61    fn to_parameter_range(
62        range: core::ops::RangeInclusive<Self>,
63    ) -> Result<ParameterRange, ParameterError>;
64}
65
66impl RangeConvertible for i64 {
67    fn to_parameter_range(
68        range: core::ops::RangeInclusive<Self>,
69    ) -> Result<ParameterRange, ParameterError> {
70        Ok(ParameterRange::Integer(crate::IntegerRange::new(
71            *range.start(),
72            *range.end(),
73            1, // Default step of 1
74        )))
75    }
76}
77
78impl RangeConvertible for f64 {
79    fn to_parameter_range(
80        range: core::ops::RangeInclusive<Self>,
81    ) -> Result<ParameterRange, ParameterError> {
82        Ok(ParameterRange::FloatingPoint(
83            crate::FloatingPointRange::new(
84                *range.start(),
85                *range.end(),
86                0.0, // No step constraint (any value in range is valid)
87            ),
88        ))
89    }
90}
91
92/// Builder for declaring a typed parameter
93pub struct ParameterBuilder<'a, 's, T: ParameterVariant> {
94    /// Borrow of the parameter server. phase-382 W2' — the server itself
95    /// borrows its table, so every handle onto it carries BOTH lifetimes:
96    /// `'a` is this borrow, `'s` is the caller-owned storage underneath.
97    server: &'a mut ParameterServer<'s>,
98    /// phase-426 W1 — the node this parameter belongs to. Fixed when the
99    /// builder is made, so a chained call cannot lose it and land the
100    /// declaration on a sibling.
101    node: NodeKey,
102    /// Parameter name
103    name: &'a str,
104    /// Default value
105    default: Option<T>,
106    /// Human-readable description
107    description: Option<&'a str>,
108    /// phase-417 W4.a — the descriptor's other free text,
109    /// `rcl_interfaces/msg/ParameterDescriptor::additional_constraints`, which
110    /// `ros2 param describe` prints.
111    constraints: Option<&'a str>,
112    /// Range constraints
113    range: Option<ParameterRange>,
114    /// Whether the parameter is read-only
115    read_only: bool,
116    /// Phantom data to hold the type parameter
117    _phantom: core::marker::PhantomData<T>,
118}
119
120impl<'a, 's, T: ParameterVariant> ParameterBuilder<'a, 's, T> {
121    /// Create a new parameter builder
122    pub fn new(server: &'a mut ParameterServer<'s>, node: NodeKey, name: &'a str) -> Self {
123        Self {
124            server,
125            node,
126            name,
127            default: None,
128            description: None,
129            constraints: None,
130            range: None,
131            read_only: false,
132            _phantom: core::marker::PhantomData,
133        }
134    }
135
136    /// Set a default value for the parameter
137    ///
138    /// The ONE default setter. rclrs also has `default_from_iter(impl IntoIterator)`
139    /// and `default_string_array(&[&str])`; neither exists here, so pass the array
140    /// value itself. An array is a `heapless::Vec` bounded by `MAX_ARRAY_LEN` (a
141    /// string array is `Vec<String<MAX_STRING_VALUE_LEN>, MAX_ARRAY_LEN>`, two
142    /// compile-time bounds), and an over-capacity value is refused by
143    /// [`ParameterVariant::try_to_parameter_value`], never truncated. No allocator.
144    pub fn default(mut self, value: T) -> Self {
145        self.default = Some(value);
146        self
147    }
148
149    /// Set a human-readable description for the parameter
150    pub fn description(mut self, desc: &'a str) -> Self {
151        self.description = Some(desc);
152        self
153    }
154
155    /// Set the free-text `additional_constraints` — rclrs's
156    /// `ParameterBuilder::constraints`, and rclc's fourth argument to
157    /// `rclc_add_parameter_description` (phase-417 W4.a).
158    ///
159    /// Text longer than `NROS_MAX_PARAM_CONSTRAINTS_LEN` — whose default is
160    /// **0**, so an image pays for this field only when it asks for it — is
161    /// truncated at a character boundary and REPORTED once through
162    /// [`ParameterServer::take_truncated_descriptions`], never dropped in
163    /// silence.
164    ///
165    /// [`ParameterServer::take_truncated_descriptions`]: crate::ParameterServer::take_truncated_descriptions
166    pub fn constraints(mut self, text: &'a str) -> Self {
167        self.constraints = Some(text);
168        self
169    }
170
171    /// Set integer range constraints for the parameter
172    pub fn integer_range(mut self, min: i64, max: i64, step: i64) -> Result<Self, ParameterError> {
173        if T::parameter_type() != ParameterType::Integer {
174            return Err(ParameterError::InvalidRange);
175        }
176        let range = crate::IntegerRange::new(min, max, step);
177        // Issue 1150 — `min > max` or a negative step is refused here, where
178        // the caller can see it, not silently stored and published.
179        if !range.is_valid() {
180            return Err(ParameterError::InvalidRange);
181        }
182        self.range = Some(ParameterRange::Integer(range));
183        Ok(self)
184    }
185
186    /// Set floating point range constraints for the parameter
187    pub fn float_range(mut self, min: f64, max: f64, step: f64) -> Result<Self, ParameterError> {
188        if T::parameter_type() != ParameterType::Double {
189            return Err(ParameterError::InvalidRange);
190        }
191        let range = crate::FloatingPointRange::new(min, max, step);
192        if !range.is_valid() {
193            return Err(ParameterError::InvalidRange);
194        }
195        self.range = Some(ParameterRange::FloatingPoint(range));
196        Ok(self)
197    }
198
199    /// Set range constraints using an inclusive range
200    ///
201    /// This is a convenience method that works with `RangeInclusive`:
202    /// - For `i64` parameters: `range(0..=100)` sets an integer range with step 1
203    /// - For `f64` parameters: `range(0.0..=1.0)` sets a floating point range with step 0.0
204    ///
205    /// For more control (e.g., custom step), use `integer_range()` or `float_range()`.
206    pub fn range(mut self, range: core::ops::RangeInclusive<T>) -> Result<Self, ParameterError>
207    where
208        T: RangeConvertible,
209    {
210        self.range = Some(T::to_parameter_range(range)?);
211        Ok(self)
212    }
213
214    /// The descriptor the three terminal verbs declare with — ONE
215    /// construction (phase-417 W4.a).
216    ///
217    /// It was written out three times, identically but for the `read_only`
218    /// line, which is why `additional_constraints` would otherwise have needed
219    /// adding in three places and reached whichever two somebody remembered.
220    /// `read_only` stays at the call site because that is the one line the
221    /// three genuinely disagree about.
222    ///
223    /// phase-446 F2 -- a too-long description is truncated and reported, never
224    /// a reason to refuse the declaration: the contract derives the string
225    /// capacity to 0 for a scalar-only image, and this used to fail every
226    /// `.description(..)` there with `StringConversion`. The same holds for
227    /// the constraints text, whose capacity DEFAULTS to 0.
228    fn build_descriptor(&self) -> Result<ParameterDescriptor, ParameterError> {
229        let mut descriptor = ParameterDescriptor::new(self.name, T::parameter_type())
230            .ok_or(ParameterError::StorageFull)?;
231        if let Some(desc) = self.description {
232            descriptor.set_description(desc);
233        }
234        if let Some(text) = self.constraints {
235            descriptor.set_additional_constraints(text);
236        }
237        descriptor.range = self.range.unwrap_or_default();
238        Ok(descriptor)
239    }
240
241    /// Declare a read-only parameter
242    ///
243    /// Read-only parameters cannot be changed after declaration.
244    /// A default value must be provided.
245    pub fn read_only(self) -> Result<ReadOnlyParameter<'a, 's, T>, ParameterError> {
246        // Must have a default value for read-only parameters
247        let default_value = self
248            .default
249            .as_ref()
250            .ok_or(ParameterError::NotFound)?
251            .clone();
252
253        let mut descriptor = self.build_descriptor()?;
254        descriptor.read_only = true;
255
256        let param_value = default_value.to_parameter_value();
257
258        self.server
259            .declare_parameter(self.node, descriptor, Some(&param_value))?;
260
261        Ok(ReadOnlyParameter::new(self.server, self.node, self.name))
262    }
263
264    /// Declare a mandatory parameter
265    ///
266    /// If no default value is provided, it must be set externally before use.
267    pub fn mandatory(self) -> Result<MandatoryParameter<'a, 's, T>, ParameterError> {
268        let mut descriptor = self.build_descriptor()?;
269        descriptor.read_only = self.read_only;
270
271        let default_value = self.default.map(|v| v.to_parameter_value());
272
273        self.server
274            .declare_parameter(self.node, descriptor, default_value.as_ref())?;
275
276        Ok(MandatoryParameter::new(self.server, self.node, self.name))
277    }
278
279    /// Declare an optional parameter
280    pub fn optional(self) -> Result<OptionalParameter<'a, 's, T>, ParameterError> {
281        let mut descriptor = self.build_descriptor()?;
282        descriptor.read_only = self.read_only;
283
284        let default_value = self.default.map(|v| v.to_parameter_value());
285
286        self.server
287            .declare_parameter(self.node, descriptor, default_value.as_ref())?;
288
289        Ok(OptionalParameter::new(self.server, self.node, self.name))
290    }
291}
292
293/// A parameter that must always have a value
294pub struct MandatoryParameter<'a, 's, T: ParameterVariant> {
295    server: &'a mut ParameterServer<'s>,
296    /// phase-426 W1 — the node whose table this handle reads and writes.
297    node: NodeKey,
298    name: String<{ crate::MAX_PARAM_NAME_LEN }>,
299    _phantom: core::marker::PhantomData<T>,
300}
301
302impl<'a, 's, T: ParameterVariant> MandatoryParameter<'a, 's, T> {
303    pub(crate) fn new(server: &'a mut ParameterServer<'s>, node: NodeKey, name: &'a str) -> Self {
304        let mut n = String::new();
305        n.push_str(name).unwrap();
306        Self {
307            server,
308            node,
309            name: n,
310            _phantom: core::marker::PhantomData,
311        }
312    }
313
314    /// Get the current value of the parameter
315    pub fn get(&self) -> T {
316        self.server
317            .get_parameter_value(self.node, self.name.as_str())
318            .and_then(|val| T::from_parameter_value(&val))
319            .expect("Mandatory parameter must have a value")
320    }
321
322    /// Set the value of the parameter
323    pub fn set(&mut self, value: T) -> Result<(), ParameterError> {
324        // issue 0323 — reject an over-capacity value here rather than letting
325        // `to_parameter_value`'s `unwrap_or_default()` turn it into `NotSet`
326        // (a TYPE change) or an EMPTY array and then reporting success.
327        let converted = value
328            .try_to_parameter_value()
329            .map_err(|_| ParameterError::StringConversion)?;
330        let result = self
331            .server
332            .set_parameter_value(self.node, self.name.as_str(), converted);
333        if result == SetParameterResult::Success {
334            Ok(())
335        } else {
336            Err(ParameterError::from(result))
337        }
338    }
339}
340
341/// A parameter that may or may not have a value
342pub struct OptionalParameter<'a, 's, T: ParameterVariant> {
343    server: &'a mut ParameterServer<'s>,
344    /// phase-426 W1 — the node whose table this handle reads and writes.
345    node: NodeKey,
346    name: String<{ crate::MAX_PARAM_NAME_LEN }>,
347    _phantom: core::marker::PhantomData<T>,
348}
349
350impl<'a, 's, T: ParameterVariant> OptionalParameter<'a, 's, T> {
351    pub(crate) fn new(server: &'a mut ParameterServer<'s>, node: NodeKey, name: &'a str) -> Self {
352        let mut n = String::new();
353        n.push_str(name).unwrap();
354        Self {
355            server,
356            node,
357            name: n,
358            _phantom: core::marker::PhantomData,
359        }
360    }
361
362    /// Get the current value of the parameter, if set
363    pub fn get(&self) -> Option<T> {
364        self.server
365            .get_parameter_value(self.node, self.name.as_str())
366            .and_then(|val| T::from_parameter_value(&val))
367    }
368
369    /// Set the value of the parameter
370    pub fn set(&mut self, value: Option<T>) -> Result<(), ParameterError> {
371        let param_value = value.map(|v| v.to_parameter_value());
372        let result = self.server.set_parameter_value(
373            self.node,
374            self.name.as_str(),
375            param_value.unwrap_or_default(),
376        );
377        if result == SetParameterResult::Success {
378            Ok(())
379        } else {
380            Err(ParameterError::from(result))
381        }
382    }
383}
384
385/// A parameter whose value cannot be changed after declaration
386pub struct ReadOnlyParameter<'a, 's, T: ParameterVariant> {
387    server: &'a mut ParameterServer<'s>,
388    /// phase-426 W1 — the node whose table this handle reads and writes.
389    node: NodeKey,
390    name: String<{ crate::MAX_PARAM_NAME_LEN }>,
391    _phantom: core::marker::PhantomData<T>,
392}
393
394impl<'a, 's, T: ParameterVariant> ReadOnlyParameter<'a, 's, T> {
395    pub(crate) fn new(server: &'a mut ParameterServer<'s>, node: NodeKey, name: &'a str) -> Self {
396        let mut n = String::new();
397        n.push_str(name).unwrap();
398        Self {
399            server,
400            node,
401            name: n,
402            _phantom: core::marker::PhantomData,
403        }
404    }
405
406    /// Get the current value of the parameter
407    pub fn get(&self) -> T {
408        self.server
409            .get_parameter_value(self.node, self.name.as_str())
410            .and_then(|val| T::from_parameter_value(&val))
411            .expect("Read-only parameter must have a value")
412    }
413}
414
415/// Provides access to undeclared parameters in a ParameterServer
416///
417/// This struct is returned by `Node::use_undeclared_parameters()` and allows
418/// for dynamic retrieval of parameter values by name without explicit declaration.
419///
420/// rclrs calls this type `Parameters`; the capability is the same (read AND write,
421/// by name), only the name differs. A ported file rarely names the type — it binds
422/// what `use_undeclared_parameters()` returns — so the edit is a `use` line.
423pub struct UndeclaredParameters<'a, 's> {
424    server: &'a mut ParameterServer<'s>,
425    /// phase-426 W1 — whose undeclared parameters. Without this the accessor
426    /// reads whatever node happened to declare the name first.
427    node: NodeKey,
428}
429
430impl<'a, 's> UndeclaredParameters<'a, 's> {
431    pub fn new(server: &'a mut ParameterServer<'s>, node: NodeKey) -> Self {
432        Self { server, node }
433    }
434
435    /// Try to get the value of an undeclared boolean parameter
436    pub fn get_bool(&self, name: &str) -> Option<bool> {
437        self.server.get_bool(self.node, name)
438    }
439
440    /// Try to get the value of an undeclared integer parameter
441    pub fn get_integer(&self, name: &str) -> Option<i64> {
442        self.server.get_integer(self.node, name)
443    }
444
445    /// Try to get the value of an undeclared double parameter
446    pub fn get_double(&self, name: &str) -> Option<f64> {
447        self.server.get_double(self.node, name)
448    }
449
450    /// Try to get the value of an undeclared string parameter
451    pub fn get_string(&self, name: &str) -> Option<&str> {
452        self.server.get_string(self.node, name)
453    }
454
455    // ---- writes (phase-417 W4.a) -------------------------------------------
456    //
457    // rclrs's `Parameters` handle reads AND writes; ours only read, which made
458    // `use_undeclared_parameters` half a handle. The writes go through
459    // `ParameterServer::set_or_declare` rather than `apply`, and that is the
460    // whole point of the type: `apply` refuses a name the node never declared
461    // unless the node opted in, and this handle IS the opt-in — a caller
462    // reaches it by naming it, exactly as the store's own doc comment for
463    // `set_or_declare` describes.
464
465    /// Set (declaring if absent) an undeclared boolean parameter.
466    pub fn set_bool(&mut self, name: &str, value: bool) -> Result<(), ParameterError> {
467        self.set_value(name, crate::ParameterValue::Bool(value))
468    }
469
470    /// Set (declaring if absent) an undeclared integer parameter.
471    pub fn set_integer(&mut self, name: &str, value: i64) -> Result<(), ParameterError> {
472        self.set_value(name, crate::ParameterValue::Integer(value))
473    }
474
475    /// Set (declaring if absent) an undeclared double parameter.
476    pub fn set_double(&mut self, name: &str, value: f64) -> Result<(), ParameterError> {
477        self.set_value(name, crate::ParameterValue::Double(value))
478    }
479
480    /// Set (declaring if absent) an undeclared string parameter.
481    pub fn set_string(&mut self, name: &str, value: &str) -> Result<(), ParameterError> {
482        let value =
483            crate::ParameterValue::from_string(value).ok_or(ParameterError::StringConversion)?;
484        self.set_value(name, value)
485    }
486
487    fn set_value(
488        &mut self,
489        name: &str,
490        value: crate::ParameterValue,
491    ) -> Result<(), ParameterError> {
492        match self.server.set_or_declare(self.node, name, value) {
493            SetParameterResult::Success => Ok(()),
494            other => Err(other.into()),
495        }
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    /// phase-426 W1 — the node these single-node tests are about.
504    const NODE: NodeKey = NodeKey::PRIMARY;
505
506    #[test]
507    fn test_mandatory_parameter_with_default() {
508        let mut storage: ParameterStorage = ParameterStorage::new();
509        let mut server = ParameterServer::new_in(storage.as_table());
510        let param = ParameterBuilder::<i64>::new(&mut server, NODE, "test_param")
511            .default(42)
512            .description("A test parameter")
513            .mandatory()
514            .expect("Failed to declare parameter");
515
516        assert_eq!(param.get(), 42);
517    }
518
519    #[test]
520    fn test_mandatory_parameter_set() {
521        let mut storage: ParameterStorage = ParameterStorage::new();
522        let mut server = ParameterServer::new_in(storage.as_table());
523        let mut param = ParameterBuilder::<i64>::new(&mut server, NODE, "test_param")
524            .default(0)
525            .mandatory()
526            .expect("Failed to declare parameter");
527
528        param.set(100).expect("Failed to set parameter");
529        assert_eq!(param.get(), 100);
530    }
531
532    #[test]
533    fn test_optional_parameter_none() {
534        let mut storage: ParameterStorage = ParameterStorage::new();
535        let mut server = ParameterServer::new_in(storage.as_table());
536        let param = ParameterBuilder::<i64>::new(&mut server, NODE, "test_param")
537            .optional()
538            .expect("Failed to declare parameter");
539
540        assert_eq!(param.get(), None);
541    }
542
543    #[test]
544    fn test_optional_parameter_with_default() {
545        let mut storage: ParameterStorage = ParameterStorage::new();
546        let mut server = ParameterServer::new_in(storage.as_table());
547        let param = ParameterBuilder::<i64>::new(&mut server, NODE, "test_param")
548            .default(42)
549            .optional()
550            .expect("Failed to declare parameter");
551
552        assert_eq!(param.get(), Some(42));
553    }
554
555    #[test]
556    fn test_optional_parameter_set() {
557        let mut storage: ParameterStorage = ParameterStorage::new();
558        let mut server = ParameterServer::new_in(storage.as_table());
559        let mut param = ParameterBuilder::<i64>::new(&mut server, NODE, "test_param")
560            .optional()
561            .expect("Failed to declare parameter");
562
563        param.set(Some(100)).expect("Failed to set parameter");
564        assert_eq!(param.get(), Some(100));
565    }
566
567    #[test]
568    fn test_read_only_parameter() {
569        let mut storage: ParameterStorage = ParameterStorage::new();
570        let mut server = ParameterServer::new_in(storage.as_table());
571        let param = ParameterBuilder::<i64>::new(&mut server, NODE, "readonly_param")
572            .default(42)
573            .description("A read-only parameter")
574            .read_only()
575            .expect("Failed to declare parameter");
576
577        assert_eq!(param.get(), 42);
578    }
579
580    #[test]
581    fn test_read_only_parameter_requires_default() {
582        let mut storage: ParameterStorage = ParameterStorage::new();
583        let mut server = ParameterServer::new_in(storage.as_table());
584        let result = ParameterBuilder::<i64>::new(&mut server, NODE, "readonly_param").read_only();
585
586        assert_eq!(result.err(), Some(ParameterError::NotFound));
587    }
588
589    #[test]
590    fn test_integer_range_constraint() {
591        let mut storage: ParameterStorage = ParameterStorage::new();
592        let mut server = ParameterServer::new_in(storage.as_table());
593        let mut param = ParameterBuilder::<i64>::new(&mut server, NODE, "ranged_param")
594            .default(50)
595            .integer_range(0, 100, 1)
596            .expect("Failed to set range")
597            .mandatory()
598            .expect("Failed to declare parameter");
599
600        // Valid value within range
601        param.set(75).expect("Failed to set valid value");
602        assert_eq!(param.get(), 75);
603    }
604
605    #[test]
606    fn test_float_range_constraint() {
607        let mut storage: ParameterStorage = ParameterStorage::new();
608        let mut server = ParameterServer::new_in(storage.as_table());
609        let mut param = ParameterBuilder::<f64>::new(&mut server, NODE, "float_param")
610            .default(0.5)
611            .float_range(0.0, 1.0, 0.0)
612            .expect("Failed to set range")
613            .mandatory()
614            .expect("Failed to declare parameter");
615
616        // Valid value within range
617        param.set(0.75).expect("Failed to set valid value");
618        assert_eq!(param.get(), 0.75);
619    }
620
621    #[test]
622    fn test_range_convenience_integer() {
623        let mut storage: ParameterStorage = ParameterStorage::new();
624        let mut server = ParameterServer::new_in(storage.as_table());
625        let param = ParameterBuilder::<i64>::new(&mut server, NODE, "ranged_param")
626            .default(50)
627            .range(0..=100)
628            .expect("Failed to set range")
629            .mandatory()
630            .expect("Failed to declare parameter");
631
632        assert_eq!(param.get(), 50);
633    }
634
635    #[test]
636    fn test_range_convenience_float() {
637        let mut storage: ParameterStorage = ParameterStorage::new();
638        let mut server = ParameterServer::new_in(storage.as_table());
639        let param = ParameterBuilder::<f64>::new(&mut server, NODE, "float_param")
640            .default(0.5)
641            .range(0.0..=1.0)
642            .expect("Failed to set range")
643            .mandatory()
644            .expect("Failed to declare parameter");
645
646        assert_eq!(param.get(), 0.5);
647    }
648
649    /// phase-417 W4.a -- the builder's `constraints` reaches the stored
650    /// descriptor through the ONE `build_descriptor`, from every terminal
651    /// verb, and a text that does not fit the (default 0) capacity is
652    /// REPORTED rather than dropped in silence.
653    // The comparison and the `min` below read as "always true" to clippy on a
654    // build that leaves `NROS_MAX_PARAM_CONSTRAINTS_LEN` at its default of 0 --
655    // and that is the point: the constant is a KNOB, the test is written
656    // against it so it holds at 0, at 64 and at 256, and a version specialised
657    // to whichever value this build resolved would read as coverage on the
658    // other two.
659    #[allow(clippy::absurd_extreme_comparisons, clippy::unnecessary_min_or_max)]
660    #[test]
661    fn builder_constraints_reach_the_descriptor_from_every_terminal() {
662        let text = "positive, hz";
663        let fits = text.len() <= crate::MAX_PARAM_CONSTRAINTS_LEN;
664        let stored = &text[..crate::MAX_PARAM_CONSTRAINTS_LEN.min(text.len())];
665
666        for which in 0..3u8 {
667            let mut storage: ParameterStorage = ParameterStorage::new();
668            let mut server = ParameterServer::new_in(storage.as_table());
669            {
670                let b = ParameterBuilder::<f64>::new(&mut server, NODE, "rate")
671                    .default(1.0)
672                    .description("publish rate")
673                    .constraints(text);
674                match which {
675                    0 => {
676                        b.read_only().expect("read_only");
677                    }
678                    1 => {
679                        b.mandatory().expect("mandatory");
680                    }
681                    _ => {
682                        b.optional().expect("optional");
683                    }
684                }
685            }
686            let desc = server.get_descriptor(NODE, "rate").expect("descriptor");
687            assert_eq!(desc.description.as_str(), "publish rate");
688            assert_eq!(
689                desc.additional_constraints.as_str(),
690                stored,
691                "terminal {which} lost the constraints text"
692            );
693            let mut reported = 0usize;
694            server.take_truncated_descriptions(|_, _| reported += 1);
695            assert_eq!(
696                reported,
697                usize::from(!fits),
698                "terminal {which}: a constraints string that did not fit went unreported"
699            );
700        }
701    }
702
703    #[test]
704    fn test_parameter_description() {
705        let mut storage: ParameterStorage = ParameterStorage::new();
706        let mut server = ParameterServer::new_in(storage.as_table());
707        let _param = ParameterBuilder::<i64>::new(&mut server, NODE, "described_param")
708            .default(42)
709            .description("This is a test description")
710            .mandatory()
711            .expect("Failed to declare parameter");
712
713        // Verify description was set in the server
714        let desc = server.get_descriptor(NODE, "described_param");
715        assert!(desc.is_some());
716        assert_eq!(
717            desc.unwrap().description.as_str(),
718            "This is a test description"
719        );
720    }
721
722    #[test]
723    fn test_bool_parameter() {
724        let mut storage: ParameterStorage = ParameterStorage::new();
725        let mut server = ParameterServer::new_in(storage.as_table());
726        let mut param = ParameterBuilder::<bool>::new(&mut server, NODE, "bool_param")
727            .default(false)
728            .mandatory()
729            .expect("Failed to declare parameter");
730
731        assert!(!param.get());
732        param.set(true).expect("Failed to set parameter");
733        assert!(param.get());
734    }
735
736    #[test]
737    fn test_undeclared_parameters() {
738        use crate::ParameterValue;
739
740        let mut storage: ParameterStorage = ParameterStorage::new();
741        let mut server = ParameterServer::new_in(storage.as_table());
742
743        // Set some values using set_or_declare (simulating external parameter loading)
744        server.set_or_declare(NODE, "flag", ParameterValue::Bool(true));
745        server.set_or_declare(NODE, "count", ParameterValue::Integer(42));
746        server.set_or_declare(NODE, "ratio", ParameterValue::Double(0.5));
747
748        let undeclared = UndeclaredParameters::new(&mut server, NODE);
749
750        assert_eq!(undeclared.get_bool("flag"), Some(true));
751        assert_eq!(undeclared.get_integer("count"), Some(42));
752        assert_eq!(undeclared.get_double("ratio"), Some(0.5));
753        assert_eq!(undeclared.get_string("nonexistent"), None);
754    }
755}