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/// ROS 2 parameter types
17///
18/// These match the parameter types defined in rcl_interfaces/msg/ParameterType
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20#[repr(u8)]
21pub enum ParameterType {
22    /// Parameter value not set
23    #[default]
24    NotSet = 0,
25    /// Boolean parameter
26    Bool = 1,
27    /// 64-bit signed integer parameter
28    Integer = 2,
29    /// 64-bit floating point parameter
30    Double = 3,
31    /// String parameter
32    String = 4,
33    /// Byte array parameter
34    ByteArray = 5,
35    /// Boolean array parameter
36    BoolArray = 6,
37    /// Integer array parameter
38    IntegerArray = 7,
39    /// Double array parameter
40    DoubleArray = 8,
41    /// String array parameter
42    StringArray = 9,
43}
44
45/// Parameter value container
46///
47/// Holds a parameter value of any supported type.
48/// Note: This enum is intentionally large to support all parameter types
49/// without heap allocation in embedded systems.
50#[derive(Debug, Clone, Default)]
51#[allow(clippy::large_enum_variant)]
52pub enum ParameterValue {
53    /// Value not set
54    #[default]
55    NotSet,
56    /// Boolean value
57    Bool(bool),
58    /// 64-bit signed integer value
59    Integer(i64),
60    /// 64-bit floating point value
61    Double(f64),
62    /// String value
63    String(String<MAX_STRING_VALUE_LEN>),
64    /// Byte array value
65    ByteArray(Vec<u8, MAX_BYTE_ARRAY_LEN>),
66    /// Boolean array value
67    BoolArray(Vec<bool, MAX_ARRAY_LEN>),
68    /// Integer array value
69    IntegerArray(Vec<i64, MAX_ARRAY_LEN>),
70    /// Double array value
71    DoubleArray(Vec<f64, MAX_ARRAY_LEN>),
72    /// String array value (array of fixed-size strings)
73    StringArray(Vec<String<MAX_STRING_VALUE_LEN>, MAX_ARRAY_LEN>),
74}
75
76impl ParameterValue {
77    /// Get the parameter type for this value
78    pub fn param_type(&self) -> ParameterType {
79        match self {
80            Self::NotSet => ParameterType::NotSet,
81            Self::Bool(_) => ParameterType::Bool,
82            Self::Integer(_) => ParameterType::Integer,
83            Self::Double(_) => ParameterType::Double,
84            Self::String(_) => ParameterType::String,
85            Self::ByteArray(_) => ParameterType::ByteArray,
86            Self::BoolArray(_) => ParameterType::BoolArray,
87            Self::IntegerArray(_) => ParameterType::IntegerArray,
88            Self::DoubleArray(_) => ParameterType::DoubleArray,
89            Self::StringArray(_) => ParameterType::StringArray,
90        }
91    }
92
93    /// Check if the value is set
94    pub fn is_set(&self) -> bool {
95        !matches!(self, Self::NotSet)
96    }
97
98    /// Try to get the value as a bool
99    pub fn as_bool(&self) -> Option<bool> {
100        match self {
101            Self::Bool(v) => Some(*v),
102            _ => None,
103        }
104    }
105
106    /// Try to get the value as an integer
107    pub fn as_integer(&self) -> Option<i64> {
108        match self {
109            Self::Integer(v) => Some(*v),
110            _ => None,
111        }
112    }
113
114    /// Try to get the value as a double
115    pub fn as_double(&self) -> Option<f64> {
116        match self {
117            Self::Double(v) => Some(*v),
118            _ => None,
119        }
120    }
121
122    /// Try to get the value as a string slice
123    pub fn as_string(&self) -> Option<&str> {
124        match self {
125            Self::String(v) => Some(v.as_str()),
126            _ => None,
127        }
128    }
129
130    /// Try to get the value as a byte array slice
131    pub fn as_byte_array(&self) -> Option<&[u8]> {
132        match self {
133            Self::ByteArray(v) => Some(v.as_slice()),
134            _ => None,
135        }
136    }
137
138    /// Try to get the value as a bool array slice
139    pub fn as_bool_array(&self) -> Option<&[bool]> {
140        match self {
141            Self::BoolArray(v) => Some(v.as_slice()),
142            _ => None,
143        }
144    }
145
146    /// Try to get the value as an integer array slice
147    pub fn as_integer_array(&self) -> Option<&[i64]> {
148        match self {
149            Self::IntegerArray(v) => Some(v.as_slice()),
150            _ => None,
151        }
152    }
153
154    /// Try to get the value as a double array slice
155    pub fn as_double_array(&self) -> Option<&[f64]> {
156        match self {
157            Self::DoubleArray(v) => Some(v.as_slice()),
158            _ => None,
159        }
160    }
161
162    /// Create a bool value
163    pub fn from_bool(value: bool) -> Self {
164        Self::Bool(value)
165    }
166
167    /// Create an integer value
168    pub fn from_integer(value: i64) -> Self {
169        Self::Integer(value)
170    }
171
172    /// Create a double value
173    pub fn from_double(value: f64) -> Self {
174        Self::Double(value)
175    }
176
177    /// Create a string value from a str slice
178    pub fn from_string(value: &str) -> Option<Self> {
179        let mut s = String::new();
180        s.push_str(value).ok()?;
181        Some(Self::String(s))
182    }
183}
184
185/// Floating point range constraints for parameters
186#[derive(Debug, Clone, Copy, Default)]
187pub struct FloatingPointRange {
188    /// Minimum allowed value (inclusive)
189    pub min: f64,
190    /// Maximum allowed value (inclusive)
191    pub max: f64,
192    /// Step size for value changes (0 = any step allowed)
193    pub step: f64,
194}
195
196impl FloatingPointRange {
197    /// Create a new floating point range
198    pub const fn new(min: f64, max: f64, step: f64) -> Self {
199        Self { min, max, step }
200    }
201
202    /// Check if a value is within this range
203    pub fn contains(&self, value: f64) -> bool {
204        value >= self.min && value <= self.max
205    }
206}
207
208/// Integer range constraints for parameters
209#[derive(Debug, Clone, Copy, Default)]
210pub struct IntegerRange {
211    /// Minimum allowed value (inclusive)
212    pub min: i64,
213    /// Maximum allowed value (inclusive)
214    pub max: i64,
215    /// Step size for value changes (0 = any step allowed)
216    pub step: i64,
217}
218
219impl IntegerRange {
220    /// Create a new integer range
221    pub const fn new(min: i64, max: i64, step: i64) -> Self {
222        Self { min, max, step }
223    }
224
225    /// Check if a value is within this range
226    pub fn contains(&self, value: i64) -> bool {
227        value >= self.min && value <= self.max
228    }
229}
230
231/// Range constraints for a parameter
232#[derive(Debug, Clone, Copy, Default)]
233pub enum ParameterRange {
234    /// No range constraints
235    #[default]
236    None,
237    /// Floating point range
238    FloatingPoint(FloatingPointRange),
239    /// Integer range
240    Integer(IntegerRange),
241}
242
243/// Parameter descriptor containing metadata
244///
245/// Describes a parameter including its type, constraints, and documentation.
246#[derive(Debug, Clone)]
247pub struct ParameterDescriptor {
248    /// Parameter name
249    pub name: String<MAX_PARAM_NAME_LEN>,
250    /// Parameter type
251    pub param_type: ParameterType,
252    /// Human-readable description
253    pub description: String<MAX_STRING_VALUE_LEN>,
254    /// Whether the parameter is read-only
255    pub read_only: bool,
256    /// Whether the parameter type can change dynamically
257    pub dynamic_typing: bool,
258    /// Range constraints
259    pub range: ParameterRange,
260}
261
262impl ParameterDescriptor {
263    /// Create a new parameter descriptor
264    pub fn new(name: &str, param_type: ParameterType) -> Option<Self> {
265        let mut n = String::new();
266        n.push_str(name).ok()?;
267        Some(Self {
268            name: n,
269            param_type,
270            description: String::new(),
271            read_only: false,
272            dynamic_typing: false,
273            range: ParameterRange::None,
274        })
275    }
276
277    /// Set the description
278    pub fn with_description(mut self, desc: &str) -> Self {
279        self.description.clear();
280        let _ = self.description.push_str(desc);
281        self
282    }
283
284    /// Set read-only flag
285    pub fn with_read_only(mut self, read_only: bool) -> Self {
286        self.read_only = read_only;
287        self
288    }
289
290    /// Set dynamic typing flag
291    pub fn with_dynamic_typing(mut self, dynamic: bool) -> Self {
292        self.dynamic_typing = dynamic;
293        self
294    }
295
296    /// Set integer range constraints
297    pub fn with_integer_range(mut self, min: i64, max: i64, step: i64) -> Self {
298        self.range = ParameterRange::Integer(IntegerRange::new(min, max, step));
299        self
300    }
301
302    /// Set floating point range constraints
303    pub fn with_float_range(mut self, min: f64, max: f64, step: f64) -> Self {
304        self.range = ParameterRange::FloatingPoint(FloatingPointRange::new(min, max, step));
305        self
306    }
307
308    /// Check if a value satisfies the range constraints
309    pub fn validate_range(&self, value: &ParameterValue) -> bool {
310        match (&self.range, value) {
311            (ParameterRange::None, _) => true,
312            (ParameterRange::Integer(range), ParameterValue::Integer(v)) => range.contains(*v),
313            (ParameterRange::FloatingPoint(range), ParameterValue::Double(v)) => range.contains(*v),
314            _ => true,
315        }
316    }
317}
318
319/// A named parameter with value and optional descriptor
320#[derive(Debug, Clone)]
321pub struct Parameter {
322    /// Parameter name
323    pub name: String<MAX_PARAM_NAME_LEN>,
324    /// Parameter value
325    pub value: ParameterValue,
326}
327
328impl Parameter {
329    /// Create a new parameter with a value
330    pub fn new(name: &str, value: ParameterValue) -> Option<Self> {
331        let mut n = String::new();
332        n.push_str(name).ok()?;
333        Some(Self { name: n, value })
334    }
335
336    /// Create a new boolean parameter
337    pub fn bool(name: &str, value: bool) -> Option<Self> {
338        Self::new(name, ParameterValue::Bool(value))
339    }
340
341    /// Create a new integer parameter
342    pub fn integer(name: &str, value: i64) -> Option<Self> {
343        Self::new(name, ParameterValue::Integer(value))
344    }
345
346    /// Create a new double parameter
347    pub fn double(name: &str, value: f64) -> Option<Self> {
348        Self::new(name, ParameterValue::Double(value))
349    }
350
351    /// Create a new string parameter
352    pub fn string(name: &str, value: &str) -> Option<Self> {
353        Self::new(name, ParameterValue::from_string(value)?)
354    }
355
356    /// Get the parameter type
357    pub fn param_type(&self) -> ParameterType {
358        self.value.param_type()
359    }
360
361    /// Get the parameter name
362    pub fn name(&self) -> &str {
363        &self.name
364    }
365}
366
367/// Result of setting a parameter
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum SetParameterResult {
370    /// Parameter was set successfully
371    Success,
372    /// Parameter is read-only
373    ReadOnly,
374    /// Parameter type mismatch (and dynamic typing disabled)
375    TypeMismatch,
376    /// Value is outside allowed range
377    OutOfRange,
378    /// Parameter not found
379    NotFound,
380    /// Storage is full
381    StorageFull,
382}
383
384impl SetParameterResult {
385    /// Check if the result indicates success
386    pub fn is_success(&self) -> bool {
387        matches!(self, Self::Success)
388    }
389}
390
391/// A value does not fit the compile-time parameter capacity (issue 0323).
392///
393/// Returned by [`ParameterVariant::try_to_parameter_value`]. A named type
394/// rather than `()` so the failure reads at the call site and clippy's
395/// `result_unit_err` stays satisfied.
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub struct CapacityExceeded;
398
399/// Trait for types that can be used as typed parameters
400///
401/// This trait provides conversions between Rust types and ParameterValue,
402/// enabling type-safe parameter access.
403pub trait ParameterVariant: Clone {
404    /// Convert this type to a ParameterValue
405    fn to_parameter_value(&self) -> ParameterValue;
406
407    /// Fallible conversion — `Err(())` when `self` does not fit the
408    /// compile-time capacity (issue 0323).
409    ///
410    /// [`Self::to_parameter_value`] cannot report that: the hosted `std`
411    /// impls used `unwrap_or_default()`, so an over-long `String` became
412    /// `ParameterValue::NotSet` (a TYPE change) and an oversized `Vec`
413    /// became an EMPTY array, both indistinguishable from a caller who
414    /// meant it. Callers at the declare/set boundary should prefer this.
415    ///
416    /// Defaults to infallible for impls whose values cannot overflow (the
417    /// scalars); the `std` collection impls override it.
418    fn try_to_parameter_value(&self) -> Result<ParameterValue, CapacityExceeded> {
419        Ok(self.to_parameter_value())
420    }
421
422    /// Try to extract this type from a ParameterValue
423    fn from_parameter_value(value: &ParameterValue) -> Option<Self>;
424
425    /// Get the expected parameter type
426    fn parameter_type() -> ParameterType;
427}
428
429// Implement ParameterVariant for basic types
430
431impl ParameterVariant for bool {
432    fn to_parameter_value(&self) -> ParameterValue {
433        ParameterValue::Bool(*self)
434    }
435
436    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
437        value.as_bool()
438    }
439
440    fn parameter_type() -> ParameterType {
441        ParameterType::Bool
442    }
443}
444
445impl ParameterVariant for i64 {
446    fn to_parameter_value(&self) -> ParameterValue {
447        ParameterValue::Integer(*self)
448    }
449
450    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
451        value.as_integer()
452    }
453
454    fn parameter_type() -> ParameterType {
455        ParameterType::Integer
456    }
457}
458
459impl ParameterVariant for f64 {
460    fn to_parameter_value(&self) -> ParameterValue {
461        ParameterValue::Double(*self)
462    }
463
464    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
465        value.as_double()
466    }
467
468    fn parameter_type() -> ParameterType {
469        ParameterType::Double
470    }
471}
472
473// `ParameterVariant` for `heapless::String` — the fixed-capacity parameter type.
474//
475// phase-359 W10 — this was `#[cfg(not(feature = "std"))]`, which said a hosted
476// build may not use a fixed-capacity string parameter. Nothing enforced that:
477// the `alloc` impl below is on a DIFFERENT type, so an `alloc`-without-`std`
478// build has always had both, and only the hosted flavour was singled out. A
479// consumer that wants bounded parameter storage on a host — the usual reason
480// being that the same node also builds for a target — was told no by a gate
481// that was expressing a preference, not a constraint.
482impl ParameterVariant for String<MAX_STRING_VALUE_LEN> {
483    fn to_parameter_value(&self) -> ParameterValue {
484        ParameterValue::String(self.clone())
485    }
486
487    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
488        if let ParameterValue::String(s) = value {
489            Some(s.clone())
490        } else {
491            None
492        }
493    }
494
495    fn parameter_type() -> ParameterType {
496        ParameterType::String
497    }
498}
499
500// Implement ParameterVariant for std::string::String (std)
501#[cfg(feature = "alloc")]
502impl ParameterVariant for alloc::string::String {
503    fn to_parameter_value(&self) -> ParameterValue {
504        ParameterValue::from_string(self.as_str()).unwrap_or_default()
505    }
506
507    fn try_to_parameter_value(&self) -> Result<ParameterValue, CapacityExceeded> {
508        ParameterValue::from_string(self.as_str()).ok_or(CapacityExceeded)
509    }
510
511    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
512        value.as_string().map(|s| s.to_string())
513    }
514
515    fn parameter_type() -> ParameterType {
516        ParameterType::String
517    }
518}
519
520// Implement ParameterVariant for std::vec::Vec<i64> (std)
521#[cfg(feature = "alloc")]
522impl ParameterVariant for alloc::vec::Vec<i64> {
523    fn to_parameter_value(&self) -> ParameterValue {
524        ParameterValue::IntegerArray(Vec::from_slice(self.as_slice()).unwrap_or_default())
525    }
526
527    fn try_to_parameter_value(&self) -> Result<ParameterValue, CapacityExceeded> {
528        Vec::from_slice(self.as_slice())
529            .map(ParameterValue::IntegerArray)
530            .map_err(|_| CapacityExceeded)
531    }
532
533    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
534        value.as_integer_array().map(|v| v.to_vec())
535    }
536
537    fn parameter_type() -> ParameterType {
538        ParameterType::IntegerArray
539    }
540}
541
542// Implement ParameterVariant for std::vec::Vec<f64> (std)
543#[cfg(feature = "alloc")]
544impl ParameterVariant for alloc::vec::Vec<f64> {
545    fn to_parameter_value(&self) -> ParameterValue {
546        ParameterValue::DoubleArray(Vec::from_slice(self.as_slice()).unwrap_or_default())
547    }
548
549    fn try_to_parameter_value(&self) -> Result<ParameterValue, CapacityExceeded> {
550        Vec::from_slice(self.as_slice())
551            .map(ParameterValue::DoubleArray)
552            .map_err(|_| CapacityExceeded)
553    }
554
555    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
556        value.as_double_array().map(|v| v.to_vec())
557    }
558
559    fn parameter_type() -> ParameterType {
560        ParameterType::DoubleArray
561    }
562}
563
564// Implement ParameterVariant for std::vec::Vec<bool> (std)
565#[cfg(feature = "alloc")]
566impl ParameterVariant for alloc::vec::Vec<bool> {
567    fn to_parameter_value(&self) -> ParameterValue {
568        ParameterValue::BoolArray(Vec::from_slice(self.as_slice()).unwrap_or_default())
569    }
570
571    fn try_to_parameter_value(&self) -> Result<ParameterValue, CapacityExceeded> {
572        Vec::from_slice(self.as_slice())
573            .map(ParameterValue::BoolArray)
574            .map_err(|_| CapacityExceeded)
575    }
576
577    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
578        value.as_bool_array().map(|v| v.to_vec())
579    }
580
581    fn parameter_type() -> ParameterType {
582        ParameterType::BoolArray
583    }
584}
585
586// Implement ParameterVariant for std::vec::Vec<std::string::String> (std)
587#[cfg(feature = "alloc")]
588impl ParameterVariant for alloc::vec::Vec<alloc::string::String> {
589    fn to_parameter_value(&self) -> ParameterValue {
590        let mut vec = Vec::new();
591        for s in self {
592            let mut h_string = String::new();
593            if h_string.push_str(s.as_str()).is_ok() {
594                let _ = vec.push(h_string);
595            }
596        }
597        ParameterValue::StringArray(vec)
598    }
599
600    fn try_to_parameter_value(&self) -> Result<ParameterValue, CapacityExceeded> {
601        // The infallible form above SKIPS any element that does not fit,
602        // so a 3-element vec could arrive as 2 with no signal (issue 0323).
603        let mut vec = Vec::new();
604        for s in self {
605            let mut h_string = String::new();
606            h_string
607                .push_str(s.as_str())
608                .map_err(|_| CapacityExceeded)?;
609            vec.push(h_string).map_err(|_| CapacityExceeded)?;
610        }
611        Ok(ParameterValue::StringArray(vec))
612    }
613
614    fn from_parameter_value(value: &ParameterValue) -> Option<Self> {
615        if let ParameterValue::StringArray(v) = value {
616            Some(v.iter().map(|s| s.as_str().to_string()).collect())
617        } else {
618            None
619        }
620    }
621
622    fn parameter_type() -> ParameterType {
623        ParameterType::StringArray
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630
631    #[test]
632    #[allow(clippy::approx_constant)]
633    fn test_parameter_value_types() {
634        let bool_val = ParameterValue::Bool(true);
635        assert_eq!(bool_val.param_type(), ParameterType::Bool);
636        assert_eq!(bool_val.as_bool(), Some(true));
637
638        let int_val = ParameterValue::Integer(42);
639        assert_eq!(int_val.param_type(), ParameterType::Integer);
640        assert_eq!(int_val.as_integer(), Some(42));
641
642        let double_val = ParameterValue::Double(3.14);
643        assert_eq!(double_val.param_type(), ParameterType::Double);
644        assert_eq!(double_val.as_double(), Some(3.14));
645    }
646
647    #[test]
648    fn test_parameter_value_string() {
649        let string_val = ParameterValue::from_string("hello").unwrap();
650        assert_eq!(string_val.param_type(), ParameterType::String);
651        assert_eq!(string_val.as_string(), Some("hello"));
652    }
653
654    #[test]
655    fn test_parameter_creation() {
656        let param = Parameter::bool("my_param", true).unwrap();
657        assert_eq!(param.name(), "my_param");
658        assert_eq!(param.param_type(), ParameterType::Bool);
659        assert_eq!(param.value.as_bool(), Some(true));
660    }
661
662    #[test]
663    fn test_parameter_descriptor() {
664        let desc = ParameterDescriptor::new("speed", ParameterType::Double)
665            .unwrap()
666            .with_description("Maximum speed in m/s")
667            .with_float_range(0.0, 10.0, 0.1);
668
669        assert_eq!(desc.name.as_str(), "speed");
670        assert_eq!(desc.param_type, ParameterType::Double);
671        assert!(!desc.read_only);
672
673        // Test range validation
674        assert!(desc.validate_range(&ParameterValue::Double(5.0)));
675        assert!(!desc.validate_range(&ParameterValue::Double(15.0)));
676    }
677
678    #[test]
679    fn test_integer_range() {
680        let range = IntegerRange::new(0, 100, 1);
681        assert!(range.contains(50));
682        assert!(range.contains(0));
683        assert!(range.contains(100));
684        assert!(!range.contains(-1));
685        assert!(!range.contains(101));
686    }
687
688    #[test]
689    fn test_set_result() {
690        assert!(SetParameterResult::Success.is_success());
691        assert!(!SetParameterResult::ReadOnly.is_success());
692    }
693}
694
695// =============================================================================
696// Ghost model validation
697// =============================================================================
698
699#[cfg(test)]
700mod ghost_checks {
701    use super::*;
702    use nros_ghost_types::ParameterValueGhost;
703
704    /// Structural check: exhaustive match maps all 10 variants.
705    /// If a variant is added or removed, this fails to compile.
706    fn ghost_from_value(v: &ParameterValue) -> ParameterValueGhost {
707        match v {
708            ParameterValue::NotSet => ParameterValueGhost::NotSet,
709            ParameterValue::Bool(b) => ParameterValueGhost::Bool(*b),
710            ParameterValue::Integer(i) => ParameterValueGhost::Integer(*i),
711            ParameterValue::Double(_) => ParameterValueGhost::Double,
712            ParameterValue::String(_) => ParameterValueGhost::String,
713            ParameterValue::ByteArray(_) => ParameterValueGhost::ByteArray,
714            ParameterValue::BoolArray(_) => ParameterValueGhost::BoolArray,
715            ParameterValue::IntegerArray(_) => ParameterValueGhost::IntegerArray,
716            ParameterValue::DoubleArray(_) => ParameterValueGhost::DoubleArray,
717            ParameterValue::StringArray(_) => ParameterValueGhost::StringArray,
718        }
719    }
720
721    #[test]
722    fn ghost_variant_coverage() {
723        // Exhaustive match compiles — all 10 variants exist in both types
724        let _ = ghost_from_value(&ParameterValue::NotSet);
725        let _ = ghost_from_value(&ParameterValue::Bool(true));
726        let _ = ghost_from_value(&ParameterValue::Integer(0));
727        let _ = ghost_from_value(&ParameterValue::Double(0.0));
728        let _ = ghost_from_value(&ParameterValue::String(String::new()));
729        let _ = ghost_from_value(&ParameterValue::ByteArray(Vec::new()));
730        let _ = ghost_from_value(&ParameterValue::BoolArray(Vec::new()));
731        let _ = ghost_from_value(&ParameterValue::IntegerArray(Vec::new()));
732        let _ = ghost_from_value(&ParameterValue::DoubleArray(Vec::new()));
733        let _ = ghost_from_value(&ParameterValue::StringArray(Vec::new()));
734    }
735
736    #[test]
737    fn ghost_bool_preserves() {
738        let ghost = ghost_from_value(&ParameterValue::Bool(true));
739        assert!(matches!(ghost, ParameterValueGhost::Bool(true)));
740    }
741
742    #[test]
743    fn ghost_integer_preserves() {
744        let ghost = ghost_from_value(&ParameterValue::Integer(42));
745        assert!(matches!(ghost, ParameterValueGhost::Integer(42)));
746    }
747}
748
749// =============================================================================
750// Kani bounded model checking proofs
751// =============================================================================
752
753#[cfg(kani)]
754mod verification {
755    use super::*;
756
757    // ---- ParameterValue type conversions ----
758
759    #[kani::proof]
760    fn parameter_i64_roundtrip() {
761        let val: i64 = kani::any();
762        let pv = ParameterValue::from_integer(val);
763        assert_eq!(pv.as_integer(), Some(val));
764        assert_eq!(pv.param_type(), ParameterType::Integer);
765        assert!(pv.is_set());
766    }
767
768    #[kani::proof]
769    fn parameter_bool_roundtrip() {
770        let val: bool = kani::any();
771        let pv = ParameterValue::from_bool(val);
772        assert_eq!(pv.as_bool(), Some(val));
773        assert_eq!(pv.param_type(), ParameterType::Bool);
774        assert!(pv.is_set());
775    }
776
777    #[kani::proof]
778    fn parameter_double_roundtrip() {
779        let val: f64 = kani::any();
780        let pv = ParameterValue::from_double(val);
781        let result = pv.as_double();
782        assert!(result.is_some());
783        assert_eq!(result.unwrap().to_bits(), val.to_bits());
784        assert_eq!(pv.param_type(), ParameterType::Double);
785    }
786
787    #[kani::proof]
788    fn parameter_not_set_default() {
789        let pv = ParameterValue::default();
790        assert!(!pv.is_set());
791        assert_eq!(pv.param_type(), ParameterType::NotSet);
792        assert!(pv.as_bool().is_none());
793        assert!(pv.as_integer().is_none());
794        assert!(pv.as_double().is_none());
795        assert!(pv.as_string().is_none());
796    }
797
798    // ---- Type mismatch returns None ----
799
800    #[kani::proof]
801    fn parameter_type_mismatch_bool() {
802        let val: i64 = kani::any();
803        let pv = ParameterValue::from_integer(val);
804        assert!(pv.as_bool().is_none());
805        assert!(pv.as_double().is_none());
806        assert!(pv.as_string().is_none());
807    }
808
809    #[kani::proof]
810    fn parameter_type_mismatch_integer() {
811        let val: bool = kani::any();
812        let pv = ParameterValue::from_bool(val);
813        assert!(pv.as_integer().is_none());
814        assert!(pv.as_double().is_none());
815        assert!(pv.as_string().is_none());
816    }
817
818    // ---- IntegerRange ----
819
820    #[kani::proof]
821    fn integer_range_contains_bounds() {
822        let min: i64 = kani::any();
823        let max: i64 = kani::any();
824        // Constrain to bounded range to avoid subtraction overflow
825        kani::assume(min >= -1_000_000 && min <= 1_000_000);
826        kani::assume(max >= -1_000_000 && max <= 1_000_000);
827        kani::assume(min <= max);
828        let range = IntegerRange::new(min, max, 1);
829        // Endpoints must be contained
830        assert!(range.contains(min));
831        assert!(range.contains(max));
832    }
833
834    #[kani::proof]
835    fn integer_range_outside_bounds() {
836        let min: i64 = kani::any();
837        let max: i64 = kani::any();
838        kani::assume(min <= max);
839        kani::assume(min > i64::MIN); // So min-1 doesn't overflow
840        kani::assume(max < i64::MAX); // So max+1 doesn't overflow
841        let range = IntegerRange::new(min, max, 1);
842        assert!(!range.contains(min - 1));
843        assert!(!range.contains(max + 1));
844    }
845
846    // ---- FloatingPointRange ----
847
848    #[kani::proof]
849    fn float_range_contains_bounds() {
850        let min: f64 = kani::any();
851        let max: f64 = kani::any();
852        kani::assume(!min.is_nan() && !max.is_nan());
853        kani::assume(min <= max);
854        let range = FloatingPointRange::new(min, max, 0.0);
855        assert!(range.contains(min));
856        assert!(range.contains(max));
857    }
858
859    // ---- SetParameterResult ----
860
861    #[kani::proof]
862    fn set_result_success_only() {
863        // Only Success should return true from is_success()
864        assert!(SetParameterResult::Success.is_success());
865        assert!(!SetParameterResult::ReadOnly.is_success());
866        assert!(!SetParameterResult::TypeMismatch.is_success());
867        assert!(!SetParameterResult::OutOfRange.is_success());
868        assert!(!SetParameterResult::NotFound.is_success());
869        assert!(!SetParameterResult::StorageFull.is_success());
870    }
871}
872
873// The heap is what these need — the over-long values they build are a `Vec` and
874// a `String`, which `alloc` has.
875#[cfg(all(test, feature = "alloc"))]
876mod issue_0323_tests {
877    use super::*;
878
879    /// issue 0323 — an over-long hosted `String` used to become
880    /// `ParameterValue::NotSet` via `unwrap_or_default()`: a TYPE change the
881    /// caller could not distinguish from deliberately clearing the parameter.
882    #[test]
883    fn oversize_string_is_rejected_not_silently_notset() {
884        let long = "x".repeat(MAX_STRING_VALUE_LEN + 1);
885        assert!(
886            matches!(long.to_parameter_value(), ParameterValue::NotSet),
887            "documenting the legacy infallible behaviour"
888        );
889        assert!(
890            long.try_to_parameter_value().is_err(),
891            "the fallible path must reject it"
892        );
893    }
894
895    /// The array case: `unwrap_or_default()` yielded an EMPTY array.
896    #[test]
897    fn oversize_integer_array_is_rejected_not_silently_empty() {
898        let big: alloc::vec::Vec<i64> = (0..(MAX_ARRAY_LEN as i64 + 1)).collect();
899        match big.to_parameter_value() {
900            ParameterValue::IntegerArray(v) => {
901                assert!(v.is_empty(), "documenting the legacy empty-array behaviour")
902            }
903            other => panic!("unexpected {other:?}"),
904        }
905        assert!(big.try_to_parameter_value().is_err());
906    }
907
908    /// `Vec<String>` silently SKIPPED elements that did not fit.
909    #[test]
910    fn oversize_string_array_element_is_rejected_not_skipped() {
911        let big: alloc::vec::Vec<alloc::string::String> =
912            (0..(MAX_ARRAY_LEN + 1)).map(|i| i.to_string()).collect();
913        assert!(big.try_to_parameter_value().is_err());
914    }
915}