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