1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::value::StixValue;
8use crate::view::ObjectView;
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct ObservedData {
18 #[serde(rename = "type")]
20 pub type_: String,
21 pub id: String,
23 pub first_observed: String,
25 pub last_observed: String,
27 pub number_observed: u64,
29 #[serde(default)]
31 pub object_refs: Vec<String>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub objects: Option<BTreeMap<String, StixValue>>,
35 #[serde(flatten)]
37 pub additional: BTreeMap<String, StixValue>,
38}
39
40impl ObservedData {
41 pub fn sco_ids(&self) -> Vec<&str> {
44 self.object_refs.iter().map(String::as_str).collect()
45 }
46}
47
48impl ObjectView for ObservedData {
49 fn id(&self) -> Option<&str> {
50 Some(&self.id)
51 }
52
53 fn type_(&self) -> Option<&str> {
54 Some(&self.type_)
55 }
56
57 fn property(&self, name: &str) -> Option<StixValue> {
58 match name {
59 "type" => Some(StixValue::String(self.type_.clone())),
60 "id" => Some(StixValue::String(self.id.clone())),
61 "first_observed" => Some(StixValue::String(self.first_observed.clone())),
62 "last_observed" => Some(StixValue::String(self.last_observed.clone())),
63 "number_observed" => Some(StixValue::Integer(self.number_observed as i64)),
64 "object_refs" => Some(StixValue::List(
65 self.object_refs
66 .iter()
67 .map(|s| StixValue::String(s.clone()))
68 .collect(),
69 )),
70 "objects" => self.objects.clone().map(StixValue::Object),
71 other => self.additional.get(other).cloned(),
72 }
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::value::StixValue;
80 use crate::view::ObjectView;
81
82 fn sample_json() -> serde_json::Value {
83 serde_json::json!({
84 "type": "observed-data",
85 "id": "observed-data--1",
86 "first_observed": "2020-01-01T00:00:00Z",
87 "last_observed": "2020-01-01T00:05:00Z",
88 "number_observed": 3,
89 "object_refs": ["ipv4-addr--1", "domain-name--1"],
90 "x_custom": "keep-me"
91 })
92 }
93
94 #[test]
95 fn deserializes_typed_fields() {
96 let od: ObservedData = serde_json::from_value(sample_json()).unwrap();
97 assert_eq!(od.id, "observed-data--1");
98 assert_eq!(od.number_observed, 3);
99 assert_eq!(od.first_observed, "2020-01-01T00:00:00Z");
100 assert_eq!(od.object_refs, vec!["ipv4-addr--1", "domain-name--1"]);
101 }
102
103 #[test]
104 fn object_view_exposes_typed_and_custom_props() {
105 let od: ObservedData = serde_json::from_value(sample_json()).unwrap();
106 assert_eq!(od.type_(), Some("observed-data"));
107 assert_eq!(od.id(), Some("observed-data--1"));
108 assert_eq!(od.property("number_observed"), Some(StixValue::Integer(3)));
109 assert_eq!(
110 od.property("first_observed"),
111 Some(StixValue::String("2020-01-01T00:00:00Z".into()))
112 );
113 assert_eq!(
115 od.property("x_custom"),
116 Some(StixValue::String("keep-me".into()))
117 );
118 assert_eq!(od.property("nope"), None);
119 }
120
121 #[test]
122 fn sco_ids_prefers_object_refs() {
123 let od: ObservedData = serde_json::from_value(sample_json()).unwrap();
124 assert_eq!(od.sco_ids(), vec!["ipv4-addr--1", "domain-name--1"]);
125 }
126}