Skip to main content

nros_params/
types.rs

1//! Parameter types for ROS 2 compatible parameter handling
2//!
3//! This module provides types for representing ROS 2 parameters including
4//! scalar values, arrays, and parameter descriptors.
5
6use heapless::{String, Vec};
7
8// phase-359 W8 — `alloc`, not `std`: `ToString`, `String` and `Vec` all live in
9// `alloc`, so gating them on `std` withheld the `ParameterVariant` impls below
10// from `no_std + alloc` targets that can use them.
11#[cfg(feature = "alloc")]
12use alloc::string::ToString;
13
14pub use crate::config::*;
15
16/// Which node a parameter belongs to.
17///
18/// phase-426 W1 — upstream's parameters belong to a NODE, not to a process:
19/// `ros2 param list` enumerates per node and `/<node>/get_parameters` is a
20/// per-node service. An image composes several nodes onto one executor
21/// (RFC-0047), so a store with no node key makes `/talker`'s `rate` and
22/// `/listener`'s `rate` the same parameter.
23///
24/// The key is the executor's node INDEX, not a name: `nros_node::NodeId` is
25/// already a `u8` index into the executor's node table, it is already the
26/// identity the FFI persists (`rcl_node_t.node_id`, `nros_cpp_node_t.node_id`),
27/// and one byte per slot keeps the store's arena the size it was. A name would
28/// cost `MAX_PARAM_NAME_LEN` bytes per slot and would need a second answer to
29/// "which node is this" that could disagree with the table.
30///
31/// `nros-params` sits below `nros-node`, so this is the local spelling and
32/// `nros-node` converts (`impl From<NodeId> for NodeKey`).
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
34pub struct NodeKey(u8);
35
36impl NodeKey {
37    /// The executor's implicit primary node — `nros_node::NodeId::PRIMARY`.
38    pub const PRIMARY: NodeKey = NodeKey(0);
39
40    /// Build a key from a node table index.
41    pub const fn new(index: u8) -> Self {
42        NodeKey(index)
43    }
44
45    /// The node table index this key names.
46    pub const fn index(self) -> usize {
47        self.0 as usize
48    }
49
50    /// The raw index, for FFI persistence.
51    pub const fn raw(self) -> u8 {
52        self.0
53    }
54}
55
56/// A per-node bool, bounded by the key's own range.
57///
58/// phase-426 W1 — `allow_undeclared` is a NODE option upstream
59/// (`rclcpp::NodeOptions::allow_undeclared_parameters`, rclrs's
60/// `allow_undeclared`), so one node switching it on must not decide what a
61/// remote set may do to its sibling. [`NodeKey`] is a `u8`, so 256 bits covers
62/// the whole key space exactly: no ceiling to discover at runtime, 32 bytes,
63/// `const`-constructible, no allocation.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
65pub struct NodeFlags {
66    bits: [u64; 4],
67}
68
69impl NodeFlags {
70    /// All nodes off.
71    pub const fn new() -> Self {
72        Self { bits: [0; 4] }
73    }
74
75    /// Is the flag set for `node`?
76    pub const fn get(&self, node: NodeKey) -> bool {
77        let raw = node.raw() as usize;
78        self.bits[raw >> 6] & (1u64 << (raw & 63)) != 0
79    }
80
81    /// The lowest-numbered node whose flag is set, if any.
82    pub fn first(&self) -> Option<NodeKey> {
83        for (word, bits) in self.bits.iter().enumerate() {
84            if *bits != 0 {
85                return Some(NodeKey::new(
86                    (word * 64 + bits.trailing_zeros() as usize) as u8,
87                ));
88            }
89        }
90        None
91    }
92
93    /// Set or clear the flag for `node`.
94    pub fn set(&mut self, node: NodeKey, on: bool) {
95        let raw = node.raw() as usize;
96        let mask = 1u64 << (raw & 63);
97        if on {
98            self.bits[raw >> 6] |= mask;
99        } else {
100            self.bits[raw >> 6] &= !mask;
101        }
102    }
103}
104
105/// ROS 2 parameter types
106///
107/// These match the parameter types defined in rcl_interfaces/msg/ParameterType
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
109#[repr(u8)]
110pub enum ParameterType {
111    /// Parameter value not set
112    #[default]
113    NotSet = 0,
114    /// Boolean parameter
115    Bool = 1,
116    /// 64-bit signed integer parameter
117    Integer = 2,
118    /// 64-bit floating point parameter
119    Double = 3,
120    /// String parameter
121    String = 4,
122    /// Byte array parameter
123    ByteArray = 5,
124    /// Boolean array parameter
125    BoolArray = 6,
126    /// Integer array parameter
127    IntegerArray = 7,
128    /// Double array parameter
129    DoubleArray = 8,
130    /// String array parameter
131    StringArray = 9,
132}
133
134/// Parameter value container
135///
136/// Holds a parameter value of any supported type.
137/// Note: This enum is intentionally large to support all parameter types
138/// without heap allocation in embedded systems.
139#[derive(Debug, Clone, Default)]
140#[allow(clippy::large_enum_variant)]
141pub enum ParameterValue {
142    /// Value not set
143    #[default]
144    NotSet,
145    /// Boolean value
146    Bool(bool),
147    /// 64-bit signed integer value
148    Integer(i64),
149    /// 64-bit floating point value
150    Double(f64),
151    /// String value
152    String(String<MAX_STRING_VALUE_LEN>),
153    /// Byte array value
154    ByteArray(Vec<u8, MAX_BYTE_ARRAY_LEN>),
155    /// Boolean array value
156    BoolArray(Vec<bool, MAX_ARRAY_LEN>),
157    /// Integer array value
158    IntegerArray(Vec<i64, MAX_ARRAY_LEN>),
159    /// Double array value
160    DoubleArray(Vec<f64, MAX_ARRAY_LEN>),
161    /// String array value (array of fixed-size strings)
162    StringArray(Vec<String<MAX_STRING_VALUE_LEN>, MAX_ARRAY_LEN>),
163}
164
165impl ParameterValue {
166    /// Issue 2070 — copy `src` into `self` in place.
167    ///
168    /// `*self = src.clone()` builds the clone on the stack first, and a
169    /// `ParameterValue` is as large as its largest variant (~8 KiB at the
170    /// default capacities). This switches `self` to `src`'s variant with an
171    /// empty payload and copies the payload element by element, so no
172    /// full-size temporary exists. Both values share the capacities, so every
173    /// copy fits.
174    pub fn assign_from(&mut self, src: &ParameterValue) {
175        macro_rules! fill {
176            ($variant:ident, $dst:ident, $body:expr) => {{
177                if !matches!(self, Self::$variant(_)) {
178                    *self = Self::$variant(Default::default());
179                }
180                if let Self::$variant($dst) = self {
181                    $dst.clear();
182                    $body;
183                }
184            }};
185        }
186        match src {
187            Self::NotSet => *self = Self::NotSet,
188            Self::Bool(v) => *self = Self::Bool(*v),
189            Self::Integer(v) => *self = Self::Integer(*v),
190            Self::Double(v) => *self = Self::Double(*v),
191            Self::String(v) => fill!(String, d, {
192                let _ = d.push_str(v);
193            }),
194            Self::ByteArray(v) => fill!(ByteArray, d, {
195                let _ = d.extend_from_slice(v);
196            }),
197            Self::BoolArray(v) => fill!(BoolArray, d, {
198                let _ = d.extend_from_slice(v);
199            }),
200            Self::IntegerArray(v) => fill!(IntegerArray, d, {
201                let _ = d.extend_from_slice(v);
202            }),
203            Self::DoubleArray(v) => fill!(DoubleArray, d, {
204                let _ = d.extend_from_slice(v);
205            }),
206            Self::StringArray(v) => fill!(StringArray, d, {
207                for e in v {
208                    let mut s = String::new();
209                    let _ = s.push_str(e);
210                    let _ = d.push(s);
211                }
212            }),
213        }
214    }
215
216    /// Get the parameter type for this value
217    pub fn param_type(&self) -> ParameterType {
218        match self {
219            Self::NotSet => ParameterType::NotSet,
220            Self::Bool(_) => ParameterType::Bool,
221            Self::Integer(_) => ParameterType::Integer,
222            Self::Double(_) => ParameterType::Double,
223            Self::String(_) => ParameterType::String,
224            Self::ByteArray(_) => ParameterType::ByteArray,
225            Self::BoolArray(_) => ParameterType::BoolArray,
226            Self::IntegerArray(_) => ParameterType::IntegerArray,
227            Self::DoubleArray(_) => ParameterType::DoubleArray,
228            Self::StringArray(_) => ParameterType::StringArray,
229        }
230    }
231
232    /// Check if the value is set
233    pub fn is_set(&self) -> bool {
234        !matches!(self, Self::NotSet)
235    }
236
237    /// Try to get the value as a bool
238    pub fn as_bool(&self) -> Option<bool> {
239        match self {
240            Self::Bool(v) => Some(*v),
241            _ => None,
242        }
243    }
244
245    /// Try to get the value as an integer
246    pub fn as_integer(&self) -> Option<i64> {
247        match self {
248            Self::Integer(v) => Some(*v),
249            _ => None,
250        }
251    }
252
253    /// Try to get the value as a double
254    pub fn as_double(&self) -> Option<f64> {
255        match self {
256            Self::Double(v) => Some(*v),
257            _ => None,
258        }
259    }
260
261    /// Try to get the value as a string slice
262    pub fn as_string(&self) -> Option<&str> {
263        match self {
264            Self::String(v) => Some(v.as_str()),
265            _ => None,
266        }
267    }
268
269    /// Try to get the value as a byte array slice
270    pub fn as_byte_array(&self) -> Option<&[u8]> {
271        match self {
272            Self::ByteArray(v) => Some(v.as_slice()),
273            _ => None,
274        }
275    }
276
277    /// Try to get the value as a bool array slice
278    pub fn as_bool_array(&self) -> Option<&[bool]> {
279        match self {
280            Self::BoolArray(v) => Some(v.as_slice()),
281            _ => None,
282        }
283    }
284
285    /// Try to get the value as an integer array slice
286    pub fn as_integer_array(&self) -> Option<&[i64]> {
287        match self {
288            Self::IntegerArray(v) => Some(v.as_slice()),
289            _ => None,
290        }
291    }
292
293    /// Try to get the value as a double array slice
294    pub fn as_double_array(&self) -> Option<&[f64]> {
295        match self {
296            Self::DoubleArray(v) => Some(v.as_slice()),
297            _ => None,
298        }
299    }
300
301    /// Create a bool value
302    pub fn from_bool(value: bool) -> Self {
303        Self::Bool(value)
304    }
305
306    /// Create an integer value
307    pub fn from_integer(value: i64) -> Self {
308        Self::Integer(value)
309    }
310
311    /// Create a double value
312    pub fn from_double(value: f64) -> Self {
313        Self::Double(value)
314    }
315
316    /// Create a string value from a str slice
317    pub fn from_string(value: &str) -> Option<Self> {
318        let mut s = String::new();
319        s.push_str(value).ok()?;
320        Some(Self::String(s))
321    }
322
323    /// Create a bool-array value from a slice.
324    ///
325    /// phase-426 W4 — `None` when the slice is longer than [`MAX_ARRAY_LEN`],
326    /// the same fallible shape [`from_string`](Self::from_string) has for the
327    /// same reason: the value lives in a `heapless::Vec` inside the server's
328    /// fixed slot and there is no allocator to grow it.
329    ///
330    /// These three exist so a caller outside `nros-params` can build an array
331    /// value without naming `heapless` or the capacity constant. The C++ FFI
332    /// shim is such a caller: `nros_cpp_node_declare_param_*_array` forwards
333    /// `nros::ComponentNode`'s `std::vector<T>` facade into the executor's
334    /// store, and the capacity belongs where the storage is.
335    pub fn from_bool_array(values: &[bool]) -> Option<Self> {
336        Some(Self::BoolArray(Vec::from_slice(values).ok()?))
337    }
338
339    /// Create an integer-array value from a slice. See
340    /// [`from_bool_array`](Self::from_bool_array).
341    pub fn from_integer_array(values: &[i64]) -> Option<Self> {
342        Some(Self::IntegerArray(Vec::from_slice(values).ok()?))
343    }
344
345    /// Create a double-array value from a slice. See
346    /// [`from_bool_array`](Self::from_bool_array).
347    pub fn from_double_array(values: &[f64]) -> Option<Self> {
348        Some(Self::DoubleArray(Vec::from_slice(values).ok()?))
349    }
350
351    /// Create a byte-array value from a slice; `None` past
352    /// [`MAX_BYTE_ARRAY_LEN`]. See [`from_bool_array`](Self::from_bool_array).
353    pub fn from_byte_array(values: &[u8]) -> Option<Self> {
354        Some(Self::ByteArray(Vec::from_slice(values).ok()?))
355    }
356
357    /// Issue 2104 -- set `self` to a string, IN PLACE: the `from_*`
358    /// constructors' in-place twins, for a caller filling a slot it already
359    /// holds. `from_string(v)` returns an `Option<Self>`, and binding it puts
360    /// one or two ~8 KiB values on the caller's stack; this writes into
361    /// `self`. `false`, with `self` left as an empty value of the variant, when
362    /// `value` exceeds [`MAX_STRING_VALUE_LEN`].
363    pub fn assign_string(&mut self, value: &str) -> bool {
364        if !matches!(self, Self::String(_)) {
365            *self = Self::String(String::new());
366        }
367        match self {
368            Self::String(s) => {
369                s.clear();
370                s.push_str(value).is_ok()
371            }
372            _ => false,
373        }
374    }
375
376    /// Set `self` to a bool array in place; `false` past [`MAX_ARRAY_LEN`].
377    /// See [`assign_string`](Self::assign_string).
378    pub fn assign_bool_array(&mut self, values: &[bool]) -> bool {
379        assign_slice!(self, BoolArray, values)
380    }
381
382    /// Set `self` to an integer array in place; `false` past
383    /// [`MAX_ARRAY_LEN`]. See [`assign_string`](Self::assign_string).
384    pub fn assign_integer_array(&mut self, values: &[i64]) -> bool {
385        assign_slice!(self, IntegerArray, values)
386    }
387
388    /// Set `self` to a double array in place; `false` past
389    /// [`MAX_ARRAY_LEN`]. See [`assign_string`](Self::assign_string).
390    pub fn assign_double_array(&mut self, values: &[f64]) -> bool {
391        assign_slice!(self, DoubleArray, values)
392    }
393
394    /// Set `self` to a byte array in place; `false` past
395    /// [`MAX_BYTE_ARRAY_LEN`]. See [`assign_string`](Self::assign_string).
396    pub fn assign_byte_array(&mut self, values: &[u8]) -> bool {
397        assign_slice!(self, ByteArray, values)
398    }
399}
400
401/// The body of the `assign_*_array` family: switch `$v` to `$variant` with an
402/// empty payload when it is another variant, then copy `$values` in.
403macro_rules! assign_slice {
404    ($v:expr, $variant:ident, $values:expr) => {{
405        if !matches!($v, Self::$variant(_)) {
406            *$v = Self::$variant(Vec::new());
407        }
408        match $v {
409            Self::$variant(d) => {
410                d.clear();
411                d.extend_from_slice($values).is_ok()
412            }
413            _ => false,
414        }
415    }};
416}
417use assign_slice;
418
419/// Floating point range constraints for parameters
420#[derive(Debug, Clone, Copy, Default)]
421pub struct FloatingPointRange {
422    /// Minimum allowed value (inclusive)
423    pub min: f64,
424    /// Maximum allowed value (inclusive)
425    pub max: f64,
426    /// Step size for value changes (0 = any step allowed)
427    pub step: f64,
428}
429
430impl FloatingPointRange {
431    /// Create a new floating point range
432    pub const fn new(min: f64, max: f64, step: f64) -> Self {
433        Self { min, max, step }
434    }
435
436    /// A range is well-formed when `min <= max` and `step >= 0` (0 = no step).
437    ///
438    /// Upstream refuses to DECLARE with an ill-formed range
439    /// (`DeclarationError::InvalidRange`); a negative step here would also go
440    /// on the wire as a huge `u64` in `DescribeParameters` (issue 1150).
441    pub fn is_valid(&self) -> bool {
442        self.min <= self.max && self.step >= 0.0 && !self.step.is_nan()
443    }
444
445    /// Check if a value is within this range.
446    ///
447    /// Issue 1150: `step` is ENFORCED, mirroring rclrs `range.rs`. A value is
448    /// in range iff `min <= v <= max` and, when `step != 0`, `v` lies on the
449    /// lattice `min + k*step` within floating tolerance. Either endpoint is
450    /// always accepted, exactly as upstream (the upper bound need not be on
451    /// the lattice). The tolerance is upstream's: 100 ULP, relative to the
452    /// magnitude of the two operands.
453    pub fn contains(&self, value: f64) -> bool {
454        if are_close(value, self.min) || are_close(value, self.max) {
455            return true;
456        }
457        if !(value >= self.min && value <= self.max) {
458            return false;
459        }
460        if self.step == 0.0 {
461            return true;
462        }
463        let nearest = round_to_nearest((value - self.min) / self.step) * self.step + self.min;
464        are_close(nearest, value)
465    }
466}
467
468/// rclrs `ParameterRange<f64>::are_close`: equal within 100 ULP of the
469/// operands' magnitude. One spelling, shared by the endpoint and lattice
470/// checks so they cannot drift apart.
471#[inline]
472fn are_close(a: f64, b: f64) -> bool {
473    const ULP_TOL: f64 = 100.0;
474    (a - b).abs() <= f64::EPSILON * (a + b).abs() * ULP_TOL
475}
476
477/// Round half away from zero without `std` (`f64::round` is not in `core`
478/// on every toolchain this crate targets). Saturates through the `i64` cast
479/// for a quotient beyond `2^63`, where the lattice is not representable
480/// anyway.
481#[inline]
482fn round_to_nearest(q: f64) -> f64 {
483    if !q.is_finite() {
484        return q;
485    }
486    let shifted = if q < 0.0 { q - 0.5 } else { q + 0.5 };
487    (shifted as i64) as f64
488}
489
490/// Integer range constraints for parameters
491#[derive(Debug, Clone, Copy, Default)]
492pub struct IntegerRange {
493    /// Minimum allowed value (inclusive)
494    pub min: i64,
495    /// Maximum allowed value (inclusive)
496    pub max: i64,
497    /// Step size for value changes (0 = any step allowed)
498    pub step: i64,
499}
500
501impl IntegerRange {
502    /// Create a new integer range
503    pub const fn new(min: i64, max: i64, step: i64) -> Self {
504        Self { min, max, step }
505    }
506
507    /// A range is well-formed when `min <= max` and `step >= 0` (0 = no step).
508    /// See [`FloatingPointRange::is_valid`].
509    pub fn is_valid(&self) -> bool {
510        self.min <= self.max && self.step >= 0
511    }
512
513    /// Check if a value is within this range.
514    ///
515    /// Issue 1150: `step` is ENFORCED, mirroring rclrs `range.rs`: in range
516    /// iff `min <= v <= max` and, when `step != 0`, `(v - min)` is a multiple
517    /// of `step`. The upper bound is always accepted even when off-lattice,
518    /// exactly as upstream. Arithmetic is checked so an extreme `min` cannot
519    /// overflow into a panic.
520    pub fn contains(&self, value: i64) -> bool {
521        if !(value >= self.min && value <= self.max) {
522            return false;
523        }
524        if value == self.max || self.step == 0 {
525            return true;
526        }
527        match value
528            .checked_sub(self.min)
529            .and_then(|d| d.checked_rem(self.step))
530        {
531            Some(rem) => rem == 0,
532            // `value - min` does not fit an `i64`, so it is not `k * step`
533            // for any representable `k` the caller could have meant.
534            None => false,
535        }
536    }
537}
538
539/// Range constraints for a parameter
540#[derive(Debug, Clone, Copy, Default)]
541pub enum ParameterRange {
542    /// No range constraints
543    #[default]
544    None,
545    /// Floating point range
546    FloatingPoint(FloatingPointRange),
547    /// Integer range
548    Integer(IntegerRange),
549}
550
551impl ParameterRange {
552    /// Well-formed: `None`, or an arm whose `min <= max` and `step >= 0`.
553    pub fn is_valid(&self) -> bool {
554        match self {
555            Self::None => true,
556            Self::FloatingPoint(r) => r.is_valid(),
557            Self::Integer(r) => r.is_valid(),
558        }
559    }
560
561    /// THE range predicate. Every set — `ParameterServer::set`, the six
562    /// service handlers, the typed API — and every declare-time default check
563    /// reaches this one function, so an integer and a floating-point value
564    /// cannot be judged by two different rules (issue 1150 class).
565    ///
566    /// A value whose type does not match the arm (a string against an
567    /// integer range, a `NotSet`) is not the range's business and passes; the
568    /// type check is a separate, earlier verdict.
569    pub fn contains(&self, value: &ParameterValue) -> bool {
570        match (self, value) {
571            (Self::None, _) => true,
572            (Self::Integer(range), ParameterValue::Integer(v)) => range.contains(*v),
573            (Self::FloatingPoint(range), ParameterValue::Double(v)) => range.contains(*v),
574            _ => true,
575        }
576    }
577}
578
579/// Parameter descriptor containing metadata
580///
581/// Describes a parameter including its type, constraints, and documentation.
582#[derive(Debug, Clone)]
583pub struct ParameterDescriptor {
584    /// Parameter name
585    pub name: String<MAX_PARAM_NAME_LEN>,
586    /// Parameter type
587    pub param_type: ParameterType,
588    /// Human-readable description.
589    ///
590    /// phase-446 F2 -- its own capacity, `NROS_MAX_PARAM_DESCRIPTION_LEN`. It
591    /// used to share `MAX_STRING_VALUE_LEN`, which the contract derives to 0
592    /// for an image with no string parameter, so such an image could carry no
593    /// description at all. Write it with [`Self::set_description`], which
594    /// truncates and records the truncation instead of dropping the text.
595    pub description: String<MAX_PARAM_DESCRIPTION_LEN>,
596    /// Free-text extra constraints — `rcl_interfaces/msg/ParameterDescriptor`'s
597    /// `additional_constraints`, which `ros2 param describe` prints.
598    ///
599    /// phase-417 W4.a — its own capacity, `NROS_MAX_PARAM_CONSTRAINTS_LEN`,
600    /// **default 0**. Sharing the description's knob was the first shape and
601    /// the derived service buffer measured it wrong: the describe reply's
602    /// bound is a worst case over CAPACITY, not over use, so every image would
603    /// have paid 256 bytes per parameter for a field it never sets — 2,048
604    /// bytes on the eight-parameter island, taking its reply past the 4,096
605    /// fallback. An image that wants constraint text states the knob. Write it
606    /// with [`Self::set_additional_constraints`], which truncates at a
607    /// character boundary and records the truncation exactly as the
608    /// description does, so a value that does not fit is reported rather than
609    /// dropped in silence.
610    pub additional_constraints: String<MAX_PARAM_CONSTRAINTS_LEN>,
611    /// Whether the parameter is read-only
612    pub read_only: bool,
613    /// Whether the parameter type can change dynamically
614    pub dynamic_typing: bool,
615    /// Range constraints
616    pub range: ParameterRange,
617    /// phase-446 F2 -- the description given was longer than
618    /// `NROS_MAX_PARAM_DESCRIPTION_LEN` and `description` holds a prefix of it.
619    /// Read by [`ParameterServer::take_truncated_descriptions`](crate::ParameterServerOps::take_truncated_descriptions), which reports
620    /// each one once and clears it.
621    ///
622    /// [`ParameterServer::take_truncated_descriptions`]: crate::ParameterServerOps::take_truncated_descriptions
623    description_truncated: bool,
624}
625
626/// phase-446 F2 -- store as much of `text` as `dst` holds, cut at a UTF-8
627/// character boundary. Returns whether anything was cut.
628///
629/// `heapless::String::push_str` is all-or-nothing, so the old
630/// `let _ = push_str(..)` stored NOTHING for a description one byte too long.
631/// A prefix is what a reader of `ros2 param describe` can still use, and the
632/// `true` return is what lets the caller say it happened.
633pub fn fit_description<const N: usize>(dst: &mut String<N>, text: &str) -> bool {
634    dst.clear();
635    let mut cut = text.len().min(N);
636    while !text.is_char_boundary(cut) {
637        cut -= 1;
638    }
639    // Cannot fail: `cut <= N` bytes into an empty `String<N>`.
640    let stored = dst.push_str(&text[..cut]).is_ok();
641    debug_assert!(stored, "a prefix of at most N bytes fits String<N>");
642    cut < text.len()
643}
644
645impl ParameterDescriptor {
646    /// Create a new parameter descriptor
647    pub fn new(name: &str, param_type: ParameterType) -> Option<Self> {
648        let mut n = String::new();
649        n.push_str(name).ok()?;
650        Some(Self {
651            name: n,
652            param_type,
653            description: String::new(),
654            additional_constraints: String::new(),
655            read_only: false,
656            dynamic_typing: false,
657            range: ParameterRange::None,
658            description_truncated: false,
659        })
660    }
661
662    /// Set the description. A description longer than
663    /// `NROS_MAX_PARAM_DESCRIPTION_LEN` is truncated at a character boundary
664    /// and recorded (see [`Self::set_description`]).
665    pub fn with_description(mut self, desc: &str) -> Self {
666        self.set_description(desc);
667        self
668    }
669
670    /// phase-446 F2 -- set the description, truncating at a character
671    /// boundary when it is longer than `NROS_MAX_PARAM_DESCRIPTION_LEN`.
672    /// Returns whether it was truncated; the descriptor also remembers it, so
673    /// the executor can report it once the parameter is declared.
674    pub fn set_description(&mut self, desc: &str) -> bool {
675        self.description_truncated = fit_description(&mut self.description, desc);
676        self.description_truncated
677    }
678
679    /// Whether the last description set was truncated and not yet reported.
680    pub fn description_truncated(&self) -> bool {
681        self.description_truncated
682    }
683
684    /// phase-417 W4.a — rclc's `additional_constraints`, the fourth argument of
685    /// `rclc_add_parameter_description`. Truncated and recorded like the
686    /// description; returns whether anything was cut.
687    pub fn set_additional_constraints(&mut self, text: &str) -> bool {
688        let cut = fit_description(&mut self.additional_constraints, text);
689        // ONE truncation flag for both texts, deliberately. What the executor
690        // reports is "this parameter's descriptor prose did not fit", and the
691        // remedy — raise `NROS_MAX_PARAM_DESCRIPTION_LEN` — is the same for
692        // either field, because they share the knob. A second flag would
693        // report two states with one cure.
694        self.description_truncated |= cut;
695        cut
696    }
697
698    /// Builder form of [`Self::set_additional_constraints`].
699    pub fn with_additional_constraints(mut self, text: &str) -> Self {
700        self.set_additional_constraints(text);
701        self
702    }
703
704    /// Mark the truncation as reported (phase-446 F2 -- once per parameter).
705    pub(crate) fn clear_description_truncated(&mut self) {
706        self.description_truncated = false;
707    }
708
709    /// Set read-only flag
710    pub fn with_read_only(mut self, read_only: bool) -> Self {
711        self.read_only = read_only;
712        self
713    }
714
715    /// Set dynamic typing flag
716    pub fn with_dynamic_typing(mut self, dynamic: bool) -> Self {
717        self.dynamic_typing = dynamic;
718        self
719    }
720
721    /// Set integer range constraints
722    pub fn with_integer_range(mut self, min: i64, max: i64, step: i64) -> Self {
723        self.range = ParameterRange::Integer(IntegerRange::new(min, max, step));
724        self
725    }
726
727    /// Set floating point range constraints
728    pub fn with_float_range(mut self, min: f64, max: f64, step: f64) -> Self {
729        self.range = ParameterRange::FloatingPoint(FloatingPointRange::new(min, max, step));
730        self
731    }
732
733    /// Check if a value satisfies the range constraints — min, max AND step.
734    ///
735    /// Forwards to [`ParameterRange::contains`], the single predicate.
736    pub fn validate_range(&self, value: &ParameterValue) -> bool {
737        self.range.contains(value)
738    }
739}
740
741/// A named parameter with value and optional descriptor
742#[derive(Debug, Clone)]
743pub struct Parameter {
744    /// Parameter name
745    pub name: String<MAX_PARAM_NAME_LEN>,
746    /// Parameter value
747    pub value: ParameterValue,
748}
749
750impl Parameter {
751    /// Create a new parameter with a value
752    pub fn new(name: &str, value: ParameterValue) -> Option<Self> {
753        let mut n = String::new();
754        n.push_str(name).ok()?;
755        Some(Self { name: n, value })
756    }
757
758    /// Create a new boolean parameter
759    pub fn bool(name: &str, value: bool) -> Option<Self> {
760        Self::new(name, ParameterValue::Bool(value))
761    }
762
763    /// Create a new integer parameter
764    pub fn integer(name: &str, value: i64) -> Option<Self> {
765        Self::new(name, ParameterValue::Integer(value))
766    }
767
768    /// Create a new double parameter
769    pub fn double(name: &str, value: f64) -> Option<Self> {
770        Self::new(name, ParameterValue::Double(value))
771    }
772
773    /// Create a new string parameter
774    pub fn string(name: &str, value: &str) -> Option<Self> {
775        Self::new(name, ParameterValue::from_string(value)?)
776    }
777
778    /// Get the parameter type
779    pub fn param_type(&self) -> ParameterType {
780        self.value.param_type()
781    }
782
783    /// Get the parameter name
784    pub fn name(&self) -> &str {
785        &self.name
786    }
787}
788
789/// Result of setting a parameter
790///
791/// DEPRECATED (phase-515): the store answers `Result<(), ParameterError>`,
792/// rclrs's one spelling -- `Success` is `Ok(())` and each other verdict is the
793/// [`ParameterError`](crate::ParameterError) variant of the same name. Kept one
794/// release so out-of-tree code naming it keeps compiling.
795#[deprecated(
796    since = "0.6.0",
797    note = "the parameter store returns `Result<(), ParameterError>`; `Success` is `Ok(())`"
798)]
799#[derive(Debug, Clone, Copy, PartialEq, Eq)]
800pub enum SetParameterResult {
801    /// Parameter was set successfully
802    Success,
803    /// Parameter is read-only
804    ReadOnly,
805    /// Parameter type mismatch (and dynamic typing disabled)
806    TypeMismatch,
807    /// Value is outside allowed range
808    OutOfRange,
809    /// Parameter not found
810    NotFound,
811    /// Storage is full
812    StorageFull,
813    /// A set on a name that was never declared, and the server does not
814    /// allow undeclared parameters (issue 1151; rclrs `parameter.rs`).
815    ///
816    /// Distinct from [`NotFound`](Self::NotFound): that is what a DIRECT
817    /// `ParameterServer::set` says about a missing name; this is what the
818    /// wire-facing [`ParameterServer::apply`](crate::ParameterServerOps::apply)
819    /// says when the only remaining option — declaring it — is switched off.
820    Undeclared,
821    /// A declaration whose range is ill-formed (`min > max` or `step < 0`),
822    /// or whose default does not satisfy its own range (issue 1150; rclrs
823    /// `DeclarationError::InvalidRange` / `InitialValueOutOfRange`).
824    InvalidRange,
825    /// A registered on-set-parameters callback refused the write — rclcpp's
826    /// `SetParametersResult { successful: false }` from
827    /// `add_on_set_parameters_callback` (phase-417 W4.a).
828    ///
829    /// Distinct from every verdict above it: those are the STORE's rules, and
830    /// a caller can predict them from the descriptor. This one is the
831    /// APPLICATION's rule, and only the application knows why.
832    Rejected,
833}
834
835#[allow(deprecated)]
836impl SetParameterResult {
837    /// Check if the result indicates success
838    pub fn is_success(&self) -> bool {
839        matches!(self, Self::Success)
840    }
841}
842
843/// A value does not fit the compile-time parameter capacity (issue 0323).
844///
845/// Returned by [`ParameterVariant::try_to_parameter_value`]. A named type
846/// rather than `()` so the failure reads at the call site and clippy's
847/// `result_unit_err` stays satisfied.
848#[derive(Debug, Clone, Copy, PartialEq, Eq)]
849pub struct CapacityExceeded;
850
851/// Trait for types that can be used as typed parameters
852///
853/// This trait provides conversions between Rust types and ParameterValue,
854/// enabling type-safe parameter access.
855pub trait ParameterVariant: Clone {
856    /// Convert this type to a ParameterValue
857    fn to_parameter_value(&self) -> ParameterValue;
858
859    /// Fallible conversion — `Err(())` when `self` does not fit the
860    /// compile-time capacity (issue 0323).
861    ///
862    /// [`Self::to_parameter_value`] cannot report that: the hosted `std`
863    /// impls used `unwrap_or_default()`, so an over-long `String` became
864    /// `ParameterValue::NotSet` (a TYPE change) and an oversized `Vec`
865    /// became an EMPTY array, both indistinguishable from a caller who
866    /// meant it. Callers at the declare/set boundary should prefer this.
867    ///
868    /// Defaults to infallible for impls whose values cannot overflow (the
869    /// scalars); the `std` collection impls override it.
870    fn try_to_parameter_value(&self) -> Result<ParameterValue, CapacityExceeded> {
871        Ok(self.to_parameter_value())
872    }
873
874    /// Try to extract this type from a ParameterValue
875    fn from_parameter_value(value: &ParameterValue) -> Option<Self>;
876
877    /// Get the expected parameter type
878    fn parameter_type() -> ParameterType;
879}
880
881// Implement ParameterVariant for basic types
882
883impl ParameterVariant for bool {
884    fn to_parameter_value(&self) -> ParameterValue {
885        ParameterValue::Bool(*self)
886    }
887
888    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
889        value.as_bool()
890    }
891
892    fn parameter_type() -> ParameterType {
893        ParameterType::Bool
894    }
895}
896
897impl ParameterVariant for i64 {
898    fn to_parameter_value(&self) -> ParameterValue {
899        ParameterValue::Integer(*self)
900    }
901
902    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
903        value.as_integer()
904    }
905
906    fn parameter_type() -> ParameterType {
907        ParameterType::Integer
908    }
909}
910
911impl ParameterVariant for f64 {
912    fn to_parameter_value(&self) -> ParameterValue {
913        ParameterValue::Double(*self)
914    }
915
916    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
917        value.as_double()
918    }
919
920    fn parameter_type() -> ParameterType {
921        ParameterType::Double
922    }
923}
924
925// `ParameterVariant` for `heapless::String` — the fixed-capacity parameter type.
926//
927// phase-359 W10 — this was `#[cfg(not(feature = "std"))]`, which said a hosted
928// build may not use a fixed-capacity string parameter. Nothing enforced that:
929// the `alloc` impl below is on a DIFFERENT type, so an `alloc`-without-`std`
930// build has always had both, and only the hosted flavour was singled out. A
931// consumer that wants bounded parameter storage on a host — the usual reason
932// being that the same node also builds for a target — was told no by a gate
933// that was expressing a preference, not a constraint.
934impl ParameterVariant for String<MAX_STRING_VALUE_LEN> {
935    fn to_parameter_value(&self) -> ParameterValue {
936        ParameterValue::String(self.clone())
937    }
938
939    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
940        if let ParameterValue::String(s) = value {
941            Some(s.clone())
942        } else {
943            None
944        }
945    }
946
947    fn parameter_type() -> ParameterType {
948        ParameterType::String
949    }
950}
951
952// Implement ParameterVariant for std::string::String (std)
953#[cfg(feature = "alloc")]
954impl ParameterVariant for alloc::string::String {
955    fn to_parameter_value(&self) -> ParameterValue {
956        ParameterValue::from_string(self.as_str()).unwrap_or_default()
957    }
958
959    fn try_to_parameter_value(&self) -> Result<ParameterValue, CapacityExceeded> {
960        ParameterValue::from_string(self.as_str()).ok_or(CapacityExceeded)
961    }
962
963    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
964        value.as_string().map(|s| s.to_string())
965    }
966
967    fn parameter_type() -> ParameterType {
968        ParameterType::String
969    }
970}
971
972// Implement ParameterVariant for std::vec::Vec<i64> (std)
973#[cfg(feature = "alloc")]
974impl ParameterVariant for alloc::vec::Vec<i64> {
975    fn to_parameter_value(&self) -> ParameterValue {
976        ParameterValue::IntegerArray(Vec::from_slice(self.as_slice()).unwrap_or_default())
977    }
978
979    fn try_to_parameter_value(&self) -> Result<ParameterValue, CapacityExceeded> {
980        Vec::from_slice(self.as_slice())
981            .map(ParameterValue::IntegerArray)
982            .map_err(|_| CapacityExceeded)
983    }
984
985    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
986        value.as_integer_array().map(|v| v.to_vec())
987    }
988
989    fn parameter_type() -> ParameterType {
990        ParameterType::IntegerArray
991    }
992}
993
994// Implement ParameterVariant for std::vec::Vec<f64> (std)
995#[cfg(feature = "alloc")]
996impl ParameterVariant for alloc::vec::Vec<f64> {
997    fn to_parameter_value(&self) -> ParameterValue {
998        ParameterValue::DoubleArray(Vec::from_slice(self.as_slice()).unwrap_or_default())
999    }
1000
1001    fn try_to_parameter_value(&self) -> Result<ParameterValue, CapacityExceeded> {
1002        Vec::from_slice(self.as_slice())
1003            .map(ParameterValue::DoubleArray)
1004            .map_err(|_| CapacityExceeded)
1005    }
1006
1007    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
1008        value.as_double_array().map(|v| v.to_vec())
1009    }
1010
1011    fn parameter_type() -> ParameterType {
1012        ParameterType::DoubleArray
1013    }
1014}
1015
1016// Implement ParameterVariant for std::vec::Vec<bool> (std)
1017#[cfg(feature = "alloc")]
1018impl ParameterVariant for alloc::vec::Vec<bool> {
1019    fn to_parameter_value(&self) -> ParameterValue {
1020        ParameterValue::BoolArray(Vec::from_slice(self.as_slice()).unwrap_or_default())
1021    }
1022
1023    fn try_to_parameter_value(&self) -> Result<ParameterValue, CapacityExceeded> {
1024        Vec::from_slice(self.as_slice())
1025            .map(ParameterValue::BoolArray)
1026            .map_err(|_| CapacityExceeded)
1027    }
1028
1029    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
1030        value.as_bool_array().map(|v| v.to_vec())
1031    }
1032
1033    fn parameter_type() -> ParameterType {
1034        ParameterType::BoolArray
1035    }
1036}
1037
1038// Implement ParameterVariant for std::vec::Vec<std::string::String> (std)
1039#[cfg(feature = "alloc")]
1040impl ParameterVariant for alloc::vec::Vec<alloc::string::String> {
1041    fn to_parameter_value(&self) -> ParameterValue {
1042        let mut vec = Vec::new();
1043        for s in self {
1044            let mut h_string = String::new();
1045            if h_string.push_str(s.as_str()).is_ok() {
1046                let _ = vec.push(h_string);
1047            }
1048        }
1049        ParameterValue::StringArray(vec)
1050    }
1051
1052    fn try_to_parameter_value(&self) -> Result<ParameterValue, CapacityExceeded> {
1053        // The infallible form above SKIPS any element that does not fit,
1054        // so a 3-element vec could arrive as 2 with no signal (issue 0323).
1055        let mut vec = Vec::new();
1056        for s in self {
1057            let mut h_string = String::new();
1058            h_string
1059                .push_str(s.as_str())
1060                .map_err(|_| CapacityExceeded)?;
1061            vec.push(h_string).map_err(|_| CapacityExceeded)?;
1062        }
1063        Ok(ParameterValue::StringArray(vec))
1064    }
1065
1066    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
1067        if let ParameterValue::StringArray(v) = value {
1068            Some(v.iter().map(|s| s.as_str().to_string()).collect())
1069        } else {
1070            None
1071        }
1072    }
1073
1074    fn parameter_type() -> ParameterType {
1075        ParameterType::StringArray
1076    }
1077}
1078
1079#[cfg(test)]
1080#[allow(deprecated)] // the retired SetParameterResult keeps its own tests one release
1081mod tests {
1082    use super::*;
1083
1084    // ── issue 1150: step is ENFORCED, min/max/step as rclrs `range.rs` ──
1085
1086    #[test]
1087    fn integer_step_accepts_lattice_points_and_rejects_the_rest() {
1088        let r = IntegerRange::new(10, 20, 5);
1089        assert!(r.contains(10));
1090        assert!(r.contains(15));
1091        assert!(r.contains(20));
1092        // Inside min..max but off the lattice: this is the case the old
1093        // `min <= v <= max` accepted and upstream rejects.
1094        assert!(!r.contains(11));
1095        assert!(!r.contains(14));
1096        assert!(!r.contains(19));
1097        // Bounds still apply.
1098        assert!(!r.contains(5));
1099        assert!(!r.contains(25));
1100    }
1101
1102    #[test]
1103    fn integer_upper_bound_is_accepted_even_off_lattice() {
1104        // rclrs: `if upper == value { return true }` before the step test.
1105        let r = IntegerRange::new(0, 7, 3);
1106        assert!(r.contains(6));
1107        assert!(!r.contains(5));
1108        assert!(r.contains(7));
1109    }
1110
1111    #[test]
1112    fn integer_step_zero_means_unconstrained() {
1113        let r = IntegerRange::new(0, 100, 0);
1114        for v in [0, 1, 37, 99, 100] {
1115            assert!(r.contains(v), "{v}");
1116        }
1117        assert!(!r.contains(101));
1118    }
1119
1120    #[test]
1121    fn integer_step_does_not_overflow_at_the_extremes() {
1122        let r = IntegerRange::new(i64::MIN, i64::MAX, 2);
1123        // `v - min` overflows for every positive v; that must be a verdict,
1124        // not a panic.
1125        let _ = r.contains(1);
1126        let _ = r.contains(i64::MAX - 1);
1127        assert!(r.contains(i64::MIN));
1128        assert!(r.contains(i64::MAX));
1129    }
1130
1131    #[test]
1132    fn float_step_accepts_lattice_points_within_tolerance() {
1133        let r = FloatingPointRange::new(0.0, 1.0, 0.1);
1134        // 0.3 is not exactly representable and 0.1 * 3 != 0.3 in binary;
1135        // upstream's 100-ULP tolerance is what makes this pass.
1136        for v in [0.0, 0.1, 0.3, 0.7, 1.0] {
1137            assert!(r.contains(v), "{v}");
1138        }
1139        assert!(r.contains(0.1 + 0.2));
1140        // Off the lattice by more than the tolerance.
1141        assert!(!r.contains(0.05));
1142        assert!(!r.contains(0.35));
1143        assert!(!r.contains(0.999));
1144        // Bounds.
1145        assert!(!r.contains(-0.1));
1146        assert!(!r.contains(1.1));
1147    }
1148
1149    #[test]
1150    fn float_endpoints_are_accepted_even_off_lattice() {
1151        let r = FloatingPointRange::new(0.0, 1.0, 0.3);
1152        assert!(r.contains(0.9));
1153        assert!(!r.contains(0.95));
1154        assert!(r.contains(1.0));
1155    }
1156
1157    #[test]
1158    fn float_step_zero_means_unconstrained() {
1159        let r = FloatingPointRange::new(0.0, 10.0, 0.0);
1160        for v in [0.0, 0.001, 3.25, 9.999, 10.0] {
1161            assert!(r.contains(v), "{v}");
1162        }
1163        assert!(!r.contains(10.001));
1164    }
1165
1166    #[test]
1167    fn float_step_with_a_nonzero_min_is_relative_to_min() {
1168        let r = FloatingPointRange::new(0.5, 2.5, 0.5);
1169        assert!(r.contains(1.5));
1170        assert!(!r.contains(1.25));
1171        assert!(r.contains(0.5));
1172    }
1173
1174    #[test]
1175    fn range_validity_is_min_le_max_and_step_ge_zero() {
1176        assert!(IntegerRange::new(0, 10, 0).is_valid());
1177        assert!(IntegerRange::new(0, 10, 3).is_valid());
1178        assert!(IntegerRange::new(5, 5, 1).is_valid());
1179        assert!(!IntegerRange::new(10, 0, 1).is_valid());
1180        assert!(!IntegerRange::new(0, 10, -1).is_valid());
1181        assert!(FloatingPointRange::new(0.0, 1.0, 0.0).is_valid());
1182        assert!(!FloatingPointRange::new(1.0, 0.0, 0.1).is_valid());
1183        assert!(!FloatingPointRange::new(0.0, 1.0, -0.1).is_valid());
1184        assert!(!FloatingPointRange::new(0.0, 1.0, f64::NAN).is_valid());
1185        assert!(ParameterRange::None.is_valid());
1186    }
1187
1188    #[test]
1189    fn descriptor_validate_range_enforces_step_on_both_arms() {
1190        let int_desc = ParameterDescriptor::new("i", ParameterType::Integer)
1191            .unwrap()
1192            .with_integer_range(0, 100, 10);
1193        assert!(int_desc.validate_range(&ParameterValue::Integer(30)));
1194        assert!(!int_desc.validate_range(&ParameterValue::Integer(31)));
1195        // A value of the other type is not the range's business.
1196        assert!(int_desc.validate_range(&ParameterValue::Double(31.0)));
1197
1198        let f_desc = ParameterDescriptor::new("f", ParameterType::Double)
1199            .unwrap()
1200            .with_float_range(0.0, 10.0, 0.5);
1201        assert!(f_desc.validate_range(&ParameterValue::Double(2.5)));
1202        assert!(!f_desc.validate_range(&ParameterValue::Double(2.7)));
1203        assert!(f_desc.validate_range(&ParameterValue::Integer(3)));
1204    }
1205
1206    #[test]
1207    #[allow(clippy::approx_constant)]
1208    fn test_parameter_value_types() {
1209        let bool_val = ParameterValue::Bool(true);
1210        assert_eq!(bool_val.param_type(), ParameterType::Bool);
1211        assert_eq!(bool_val.as_bool(), Some(true));
1212
1213        let int_val = ParameterValue::Integer(42);
1214        assert_eq!(int_val.param_type(), ParameterType::Integer);
1215        assert_eq!(int_val.as_integer(), Some(42));
1216
1217        let double_val = ParameterValue::Double(3.14);
1218        assert_eq!(double_val.param_type(), ParameterType::Double);
1219        assert_eq!(double_val.as_double(), Some(3.14));
1220    }
1221
1222    #[test]
1223    fn test_parameter_value_string() {
1224        let string_val = ParameterValue::from_string("hello").unwrap();
1225        assert_eq!(string_val.param_type(), ParameterType::String);
1226        assert_eq!(string_val.as_string(), Some("hello"));
1227    }
1228
1229    #[test]
1230    fn test_parameter_creation() {
1231        let param = Parameter::bool("my_param", true).unwrap();
1232        assert_eq!(param.name(), "my_param");
1233        assert_eq!(param.param_type(), ParameterType::Bool);
1234        assert_eq!(param.value.as_bool(), Some(true));
1235    }
1236
1237    #[test]
1238    fn test_parameter_descriptor() {
1239        let desc = ParameterDescriptor::new("speed", ParameterType::Double)
1240            .unwrap()
1241            .with_description("Maximum speed in m/s")
1242            .with_float_range(0.0, 10.0, 0.1);
1243
1244        assert_eq!(desc.name.as_str(), "speed");
1245        assert_eq!(desc.param_type, ParameterType::Double);
1246        assert!(!desc.read_only);
1247
1248        // Test range validation
1249        assert!(desc.validate_range(&ParameterValue::Double(5.0)));
1250        assert!(!desc.validate_range(&ParameterValue::Double(15.0)));
1251    }
1252
1253    #[test]
1254    fn test_integer_range() {
1255        let range = IntegerRange::new(0, 100, 1);
1256        assert!(range.contains(50));
1257        assert!(range.contains(0));
1258        assert!(range.contains(100));
1259        assert!(!range.contains(-1));
1260        assert!(!range.contains(101));
1261    }
1262
1263    #[test]
1264    fn test_set_result() {
1265        assert!(SetParameterResult::Success.is_success());
1266        assert!(!SetParameterResult::ReadOnly.is_success());
1267    }
1268}
1269
1270// =============================================================================
1271// Ghost model validation
1272// =============================================================================
1273
1274#[cfg(test)]
1275mod ghost_checks {
1276    use super::*;
1277    use nros_ghost_types::ParameterValueGhost;
1278
1279    /// Structural check: exhaustive match maps all 10 variants.
1280    /// If a variant is added or removed, this fails to compile.
1281    fn ghost_from_value(v: &ParameterValue) -> ParameterValueGhost {
1282        match v {
1283            ParameterValue::NotSet => ParameterValueGhost::NotSet,
1284            ParameterValue::Bool(b) => ParameterValueGhost::Bool(*b),
1285            ParameterValue::Integer(i) => ParameterValueGhost::Integer(*i),
1286            ParameterValue::Double(_) => ParameterValueGhost::Double,
1287            ParameterValue::String(_) => ParameterValueGhost::String,
1288            ParameterValue::ByteArray(_) => ParameterValueGhost::ByteArray,
1289            ParameterValue::BoolArray(_) => ParameterValueGhost::BoolArray,
1290            ParameterValue::IntegerArray(_) => ParameterValueGhost::IntegerArray,
1291            ParameterValue::DoubleArray(_) => ParameterValueGhost::DoubleArray,
1292            ParameterValue::StringArray(_) => ParameterValueGhost::StringArray,
1293        }
1294    }
1295
1296    #[test]
1297    fn ghost_variant_coverage() {
1298        // Exhaustive match compiles — all 10 variants exist in both types
1299        let _ = ghost_from_value(&ParameterValue::NotSet);
1300        let _ = ghost_from_value(&ParameterValue::Bool(true));
1301        let _ = ghost_from_value(&ParameterValue::Integer(0));
1302        let _ = ghost_from_value(&ParameterValue::Double(0.0));
1303        let _ = ghost_from_value(&ParameterValue::String(String::new()));
1304        let _ = ghost_from_value(&ParameterValue::ByteArray(Vec::new()));
1305        let _ = ghost_from_value(&ParameterValue::BoolArray(Vec::new()));
1306        let _ = ghost_from_value(&ParameterValue::IntegerArray(Vec::new()));
1307        let _ = ghost_from_value(&ParameterValue::DoubleArray(Vec::new()));
1308        let _ = ghost_from_value(&ParameterValue::StringArray(Vec::new()));
1309    }
1310
1311    #[test]
1312    fn ghost_bool_preserves() {
1313        let ghost = ghost_from_value(&ParameterValue::Bool(true));
1314        assert!(matches!(ghost, ParameterValueGhost::Bool(true)));
1315    }
1316
1317    #[test]
1318    fn ghost_integer_preserves() {
1319        let ghost = ghost_from_value(&ParameterValue::Integer(42));
1320        assert!(matches!(ghost, ParameterValueGhost::Integer(42)));
1321    }
1322}
1323
1324// =============================================================================
1325// Kani bounded model checking proofs
1326// =============================================================================
1327
1328#[cfg(kani)]
1329mod verification {
1330    use super::*;
1331
1332    // ---- ParameterValue type conversions ----
1333
1334    #[kani::proof]
1335    fn parameter_i64_roundtrip() {
1336        let val: i64 = kani::any();
1337        let pv = ParameterValue::from_integer(val);
1338        assert_eq!(pv.as_integer(), Some(val));
1339        assert_eq!(pv.param_type(), ParameterType::Integer);
1340        assert!(pv.is_set());
1341    }
1342
1343    #[kani::proof]
1344    fn parameter_bool_roundtrip() {
1345        let val: bool = kani::any();
1346        let pv = ParameterValue::from_bool(val);
1347        assert_eq!(pv.as_bool(), Some(val));
1348        assert_eq!(pv.param_type(), ParameterType::Bool);
1349        assert!(pv.is_set());
1350    }
1351
1352    #[kani::proof]
1353    fn parameter_double_roundtrip() {
1354        let val: f64 = kani::any();
1355        let pv = ParameterValue::from_double(val);
1356        let result = pv.as_double();
1357        assert!(result.is_some());
1358        assert_eq!(result.unwrap().to_bits(), val.to_bits());
1359        assert_eq!(pv.param_type(), ParameterType::Double);
1360    }
1361
1362    #[kani::proof]
1363    fn parameter_not_set_default() {
1364        let pv = ParameterValue::default();
1365        assert!(!pv.is_set());
1366        assert_eq!(pv.param_type(), ParameterType::NotSet);
1367        assert!(pv.as_bool().is_none());
1368        assert!(pv.as_integer().is_none());
1369        assert!(pv.as_double().is_none());
1370        assert!(pv.as_string().is_none());
1371    }
1372
1373    // ---- Type mismatch returns None ----
1374
1375    #[kani::proof]
1376    fn parameter_type_mismatch_bool() {
1377        let val: i64 = kani::any();
1378        let pv = ParameterValue::from_integer(val);
1379        assert!(pv.as_bool().is_none());
1380        assert!(pv.as_double().is_none());
1381        assert!(pv.as_string().is_none());
1382    }
1383
1384    #[kani::proof]
1385    fn parameter_type_mismatch_integer() {
1386        let val: bool = kani::any();
1387        let pv = ParameterValue::from_bool(val);
1388        assert!(pv.as_integer().is_none());
1389        assert!(pv.as_double().is_none());
1390        assert!(pv.as_string().is_none());
1391    }
1392
1393    // ---- IntegerRange ----
1394
1395    #[kani::proof]
1396    fn integer_range_contains_bounds() {
1397        let min: i64 = kani::any();
1398        let max: i64 = kani::any();
1399        // Constrain to bounded range to avoid subtraction overflow
1400        kani::assume(min >= -1_000_000 && min <= 1_000_000);
1401        kani::assume(max >= -1_000_000 && max <= 1_000_000);
1402        kani::assume(min <= max);
1403        let range = IntegerRange::new(min, max, 1);
1404        // Endpoints must be contained
1405        assert!(range.contains(min));
1406        assert!(range.contains(max));
1407    }
1408
1409    #[kani::proof]
1410    fn integer_range_outside_bounds() {
1411        let min: i64 = kani::any();
1412        let max: i64 = kani::any();
1413        kani::assume(min <= max);
1414        kani::assume(min > i64::MIN); // So min-1 doesn't overflow
1415        kani::assume(max < i64::MAX); // So max+1 doesn't overflow
1416        let range = IntegerRange::new(min, max, 1);
1417        assert!(!range.contains(min - 1));
1418        assert!(!range.contains(max + 1));
1419    }
1420
1421    // ---- FloatingPointRange ----
1422
1423    #[kani::proof]
1424    fn float_range_contains_bounds() {
1425        let min: f64 = kani::any();
1426        let max: f64 = kani::any();
1427        kani::assume(!min.is_nan() && !max.is_nan());
1428        kani::assume(min <= max);
1429        let range = FloatingPointRange::new(min, max, 0.0);
1430        assert!(range.contains(min));
1431        assert!(range.contains(max));
1432    }
1433
1434    // ---- SetParameterResult ----
1435
1436    #[kani::proof]
1437    fn set_result_success_only() {
1438        // Only Success should return true from is_success()
1439        assert!(SetParameterResult::Success.is_success());
1440        assert!(!SetParameterResult::ReadOnly.is_success());
1441        assert!(!SetParameterResult::TypeMismatch.is_success());
1442        assert!(!SetParameterResult::OutOfRange.is_success());
1443        assert!(!SetParameterResult::NotFound.is_success());
1444        assert!(!SetParameterResult::StorageFull.is_success());
1445    }
1446}
1447
1448// The heap is what these need — the over-long values they build are a `Vec` and
1449// a `String`, which `alloc` has.
1450#[cfg(all(test, feature = "alloc"))]
1451mod issue_0323_tests {
1452    use super::*;
1453
1454    /// issue 0323 — an over-long hosted `String` used to become
1455    /// `ParameterValue::NotSet` via `unwrap_or_default()`: a TYPE change the
1456    /// caller could not distinguish from deliberately clearing the parameter.
1457    #[test]
1458    fn oversize_string_is_rejected_not_silently_notset() {
1459        let long = "x".repeat(MAX_STRING_VALUE_LEN + 1);
1460        assert!(
1461            matches!(long.to_parameter_value(), ParameterValue::NotSet),
1462            "documenting the legacy infallible behaviour"
1463        );
1464        assert!(
1465            long.try_to_parameter_value().is_err(),
1466            "the fallible path must reject it"
1467        );
1468    }
1469
1470    /// The array case: `unwrap_or_default()` yielded an EMPTY array.
1471    #[test]
1472    fn oversize_integer_array_is_rejected_not_silently_empty() {
1473        let big: alloc::vec::Vec<i64> = (0..(MAX_ARRAY_LEN as i64 + 1)).collect();
1474        match big.to_parameter_value() {
1475            ParameterValue::IntegerArray(v) => {
1476                assert!(v.is_empty(), "documenting the legacy empty-array behaviour")
1477            }
1478            other => panic!("unexpected {other:?}"),
1479        }
1480        assert!(big.try_to_parameter_value().is_err());
1481    }
1482
1483    /// `Vec<String>` silently SKIPPED elements that did not fit.
1484    #[test]
1485    fn oversize_string_array_element_is_rejected_not_skipped() {
1486        let big: alloc::vec::Vec<alloc::string::String> =
1487            (0..(MAX_ARRAY_LEN + 1)).map(|i| i.to_string()).collect();
1488        assert!(big.try_to_parameter_value().is_err());
1489    }
1490
1491    // -- phase-446 F2: a description has its own capacity and never vanishes --
1492
1493    /// A description that fits is stored whole and reports nothing.
1494    #[test]
1495    fn a_description_that_fits_is_stored_whole() {
1496        let mut s: String<16> = String::new();
1497        assert!(!fit_description(&mut s, "wheel speed"));
1498        assert_eq!(s.as_str(), "wheel speed");
1499        // Exactly the capacity is still a fit.
1500        assert!(!fit_description(&mut s, "0123456789abcdef"));
1501        assert_eq!(s.len(), 16);
1502    }
1503
1504    /// One byte too long used to store NOTHING (`let _ = push_str`, which is
1505    /// all-or-nothing). Now it keeps the prefix and says it cut.
1506    #[test]
1507    fn a_long_description_keeps_its_prefix_and_reports_the_cut() {
1508        let mut s: String<8> = String::new();
1509        assert!(fit_description(&mut s, "0123456789"));
1510        assert_eq!(s.as_str(), "01234567");
1511    }
1512
1513    /// The cut never splits a character: `e-acute` is two bytes and `ruler`
1514    /// (U+1F4CF) four, so a cut inside either backs off to the boundary before
1515    /// it rather than producing invalid UTF-8.
1516    #[test]
1517    fn truncation_backs_off_to_a_character_boundary() {
1518        let mut s: String<5> = String::new();
1519        // "abcd" + e-acute (bytes 4..6): byte 5 is mid-character.
1520        assert!(fit_description(&mut s, "abcd\u{e9}f"));
1521        assert_eq!(s.as_str(), "abcd");
1522        let mut s: String<6> = String::new();
1523        // "ab" + a 4-byte character (bytes 2..6) fits exactly at 6.
1524        assert!(fit_description(&mut s, "ab\u{1f4cf}z"));
1525        assert_eq!(s.as_str(), "ab\u{1f4cf}");
1526        let mut s: String<5> = String::new();
1527        assert!(fit_description(&mut s, "ab\u{1f4cf}z"));
1528        assert_eq!(s.as_str(), "ab");
1529    }
1530
1531    /// Capacity 0 is a stated "no descriptions": nothing is stored, and any
1532    /// non-empty description still reports, so the choice is visible.
1533    #[test]
1534    fn zero_capacity_stores_no_description_and_reports_it() {
1535        let mut s: String<0> = String::new();
1536        assert!(fit_description(&mut s, "anything"));
1537        assert!(s.is_empty());
1538        assert!(
1539            !fit_description(&mut s, ""),
1540            "an empty description loses nothing, so there is nothing to report"
1541        );
1542    }
1543
1544    /// The descriptor records the cut, and a fitting description clears it.
1545    #[test]
1546    fn the_descriptor_remembers_a_truncated_description() {
1547        let long = "d".repeat(MAX_PARAM_DESCRIPTION_LEN + 3);
1548        let d = ParameterDescriptor::new("p", ParameterType::Bool)
1549            .unwrap()
1550            .with_description(&long);
1551        assert_eq!(d.description.len(), MAX_PARAM_DESCRIPTION_LEN);
1552        assert!(d.description_truncated());
1553        let mut d = d;
1554        assert!(!d.set_description(""));
1555        assert!(!d.description_truncated());
1556    }
1557}