Skip to main content

stix_model/
view.rs

1//! The `ObjectView` trait and the generic value-backed object.
2
3use std::any::Any;
4use std::collections::BTreeMap;
5
6use serde::Serialize;
7
8use crate::error::{ModelError, Result};
9use crate::value::StixValue;
10
11/// A read-only, type-agnostic view over a STIX object.
12///
13/// `property` returns an *owned* [`StixValue`] so typed objects can synthesize
14/// values on demand without storing every field twice. The matcher consumes only
15/// this trait, so it never needs to branch on typed vs. generic objects.
16pub trait ObjectView {
17    /// The object's STIX id, if it has one.
18    fn id(&self) -> Option<&str>;
19    /// The object's STIX type, if it has one.
20    fn type_(&self) -> Option<&str>;
21    /// The named top-level property as an owned value, or `None` if absent.
22    fn property(&self, name: &str) -> Option<StixValue>;
23}
24
25/// A consumer-supplied object type that the matcher can view uniformly.
26///
27/// Blanket-implemented for any [`ObjectView`] that is also `Serialize`, so a
28/// consumer only writes an `ObjectView` impl. `as_json` backs serialization and
29/// equality; `as_any` enables downcasting back to the concrete type.
30pub trait CustomObject: ObjectView + std::fmt::Debug + Send + Sync {
31    /// The object serialized as a JSON value.
32    fn as_json(&self) -> serde_json::Value;
33    /// The object as `&dyn Any`, for downcasting to the concrete type.
34    fn as_any(&self) -> &dyn Any;
35}
36
37impl<T> CustomObject for T
38where
39    T: ObjectView + Serialize + std::fmt::Debug + Send + Sync + 'static,
40{
41    fn as_json(&self) -> serde_json::Value {
42        serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
43    }
44
45    fn as_any(&self) -> &dyn Any {
46        self
47    }
48}
49
50/// A STIX object stored as a flat property map. Used for any object type without
51/// a dedicated typed struct, and retains all properties (including custom ones).
52#[derive(Debug, Clone, PartialEq)]
53pub struct GenericObject {
54    properties: BTreeMap<String, StixValue>,
55}
56
57impl GenericObject {
58    /// Build a generic object from a JSON value. The value must be a JSON object.
59    pub fn from_json(value: serde_json::Value) -> Result<Self> {
60        match StixValue::from(value) {
61            StixValue::Object(properties) => Ok(GenericObject { properties }),
62            _ => Err(ModelError::InvalidObject(
63                "expected a JSON object".to_string(),
64            )),
65        }
66    }
67
68    /// The full property map.
69    pub fn properties(&self) -> &BTreeMap<String, StixValue> {
70        &self.properties
71    }
72}
73
74impl ObjectView for GenericObject {
75    fn id(&self) -> Option<&str> {
76        self.properties.get("id").and_then(StixValue::as_str)
77    }
78
79    fn type_(&self) -> Option<&str> {
80        self.properties.get("type").and_then(StixValue::as_str)
81    }
82
83    fn property(&self, name: &str) -> Option<StixValue> {
84        self.properties.get(name).cloned()
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::value::StixValue;
92
93    fn sample() -> GenericObject {
94        let v = serde_json::json!({
95            "type": "ipv4-addr",
96            "id": "ipv4-addr--1",
97            "value": "198.51.100.1"
98        });
99        GenericObject::from_json(v).unwrap()
100    }
101
102    #[test]
103    fn exposes_id_and_type() {
104        let o = sample();
105        assert_eq!(o.type_(), Some("ipv4-addr"));
106        assert_eq!(o.id(), Some("ipv4-addr--1"));
107    }
108
109    #[test]
110    fn property_returns_owned_value() {
111        let o = sample();
112        assert_eq!(
113            o.property("value"),
114            Some(StixValue::String("198.51.100.1".into()))
115        );
116        assert_eq!(o.property("missing"), None);
117    }
118
119    #[test]
120    fn rejects_non_object_json() {
121        let err = GenericObject::from_json(serde_json::json!([1, 2, 3])).unwrap_err();
122        assert!(matches!(err, crate::error::ModelError::InvalidObject(_)));
123    }
124
125    #[derive(Debug, serde::Serialize, serde::Deserialize)]
126    struct TestWidget {
127        #[serde(rename = "type")]
128        type_: String,
129        id: String,
130        risk: i64,
131    }
132
133    impl ObjectView for TestWidget {
134        fn id(&self) -> Option<&str> {
135            Some(&self.id)
136        }
137        fn type_(&self) -> Option<&str> {
138            Some(&self.type_)
139        }
140        fn property(&self, name: &str) -> Option<StixValue> {
141            match name {
142                "risk" => Some(StixValue::Integer(self.risk)),
143                _ => None,
144            }
145        }
146    }
147
148    #[test]
149    fn custom_object_blanket_impl_provides_json_and_any() {
150        let w = TestWidget {
151            type_: "x-widget".into(),
152            id: "x-widget--1".into(),
153            risk: 90,
154        };
155        // Blanket impl gives `as_json` and `as_any` for free.
156        let json = CustomObject::as_json(&w);
157        assert_eq!(json["risk"], serde_json::json!(90));
158        assert!(w.as_any().downcast_ref::<TestWidget>().is_some());
159    }
160}