1use std::any::Any;
4use std::collections::BTreeMap;
5
6use serde::Serialize;
7
8use crate::error::{ModelError, Result};
9use crate::value::StixValue;
10
11pub trait ObjectView {
17 fn id(&self) -> Option<&str>;
19 fn type_(&self) -> Option<&str>;
21 fn property(&self, name: &str) -> Option<StixValue>;
23}
24
25pub trait CustomObject: ObjectView + std::fmt::Debug + Send + Sync {
31 fn as_json(&self) -> serde_json::Value;
33 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#[derive(Debug, Clone, PartialEq)]
53pub struct GenericObject {
54 properties: BTreeMap<String, StixValue>,
55}
56
57impl GenericObject {
58 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 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 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}