Skip to main content

stix_model/
value.rs

1//! `StixValue`: a uniform, JSON-shaped value the matcher can walk.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7/// A dynamic STIX property value.
8///
9/// STIX timestamps, hex, and binary are carried as [`StixValue::String`] at this
10/// layer; higher layers interpret them. Integers and floats are kept distinct so
11/// numeric comparisons behave correctly.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13#[serde(untagged)]
14pub enum StixValue {
15    /// JSON `null`.
16    Null,
17    /// A boolean value.
18    Bool(bool),
19    /// A signed integer value.
20    Integer(i64),
21    /// A floating-point value.
22    Float(f64),
23    /// A string value (also carries timestamps, hex, and binary at this layer).
24    String(String),
25    /// An ordered list of values.
26    List(Vec<StixValue>),
27    /// A key-ordered map of property names to values.
28    Object(BTreeMap<String, StixValue>),
29}
30
31impl StixValue {
32    /// The string value, if this is a string.
33    pub fn as_str(&self) -> Option<&str> {
34        match self {
35            StixValue::String(s) => Some(s),
36            _ => None,
37        }
38    }
39
40    /// The integer value, if this is an integer.
41    pub fn as_i64(&self) -> Option<i64> {
42        match self {
43            StixValue::Integer(n) => Some(*n),
44            _ => None,
45        }
46    }
47
48    /// Returns the value as `f64`, promoting integers.
49    pub fn as_f64(&self) -> Option<f64> {
50        match self {
51            StixValue::Float(f) => Some(*f),
52            StixValue::Integer(n) => Some(*n as f64),
53            _ => None,
54        }
55    }
56
57    /// The boolean value, if this is a boolean.
58    pub fn as_bool(&self) -> Option<bool> {
59        match self {
60            StixValue::Bool(b) => Some(*b),
61            _ => None,
62        }
63    }
64
65    /// The list items, if this is a list.
66    pub fn as_list(&self) -> Option<&[StixValue]> {
67        match self {
68            StixValue::List(items) => Some(items),
69            _ => None,
70        }
71    }
72
73    /// The property map, if this is an object.
74    pub fn as_object(&self) -> Option<&BTreeMap<String, StixValue>> {
75        match self {
76            StixValue::Object(map) => Some(map),
77            _ => None,
78        }
79    }
80
81    /// Returns true if this is [`StixValue::Null`].
82    pub fn is_null(&self) -> bool {
83        matches!(self, StixValue::Null)
84    }
85}
86
87impl From<serde_json::Value> for StixValue {
88    fn from(v: serde_json::Value) -> Self {
89        use serde_json::Value;
90        match v {
91            Value::Null => StixValue::Null,
92            Value::Bool(b) => StixValue::Bool(b),
93            Value::Number(n) => {
94                if let Some(i) = n.as_i64() {
95                    StixValue::Integer(i)
96                } else {
97                    // Falls back to float for u64-too-big or fractional numbers.
98                    StixValue::Float(n.as_f64().unwrap_or(0.0))
99                }
100            }
101            Value::String(s) => StixValue::String(s),
102            Value::Array(arr) => StixValue::List(arr.into_iter().map(StixValue::from).collect()),
103            Value::Object(obj) => StixValue::Object(
104                obj.into_iter()
105                    .map(|(k, v)| (k, StixValue::from(v)))
106                    .collect(),
107            ),
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn from_json_scalars() {
118        assert_eq!(StixValue::from(serde_json::json!(null)), StixValue::Null);
119        assert_eq!(
120            StixValue::from(serde_json::json!(true)),
121            StixValue::Bool(true)
122        );
123        assert_eq!(
124            StixValue::from(serde_json::json!(42)),
125            StixValue::Integer(42)
126        );
127        assert_eq!(
128            StixValue::from(serde_json::json!(-7)),
129            StixValue::Integer(-7)
130        );
131        assert_eq!(
132            StixValue::from(serde_json::json!(2.5)),
133            StixValue::Float(2.5)
134        );
135        assert_eq!(
136            StixValue::from(serde_json::json!("hi")),
137            StixValue::String("hi".to_string())
138        );
139    }
140
141    #[test]
142    fn from_json_nested() {
143        let v = StixValue::from(serde_json::json!({"a": [1, "x"], "b": true}));
144        match v {
145            StixValue::Object(map) => {
146                assert_eq!(
147                    map.get("a"),
148                    Some(&StixValue::List(vec![
149                        StixValue::Integer(1),
150                        StixValue::String("x".to_string())
151                    ]))
152                );
153                assert_eq!(map.get("b"), Some(&StixValue::Bool(true)));
154            }
155            _ => panic!("expected object"),
156        }
157    }
158
159    #[test]
160    fn accessors() {
161        assert_eq!(StixValue::String("s".into()).as_str(), Some("s"));
162        assert_eq!(StixValue::Integer(3).as_i64(), Some(3));
163        assert_eq!(StixValue::Float(1.5).as_f64(), Some(1.5));
164        assert_eq!(StixValue::Integer(3).as_f64(), Some(3.0));
165        assert_eq!(StixValue::Bool(true).as_bool(), Some(true));
166        assert!(StixValue::Null.is_null());
167        assert_eq!(StixValue::String("s".into()).as_i64(), None);
168    }
169}