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