1use serde::{Deserialize, Serialize};
4
5use crate::error::{ModelError, Result};
6use crate::object::StixObject;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Bundle {
11 #[serde(rename = "type")]
13 pub type_: String,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub id: Option<String>,
17 #[serde(default)]
19 pub objects: Vec<StixObject>,
20}
21
22impl Bundle {
23 pub fn from_json_str(s: &str) -> Result<Self> {
25 let bundle: Bundle = serde_json::from_str(s)?;
26 if bundle.type_ != "bundle" {
27 return Err(ModelError::NotABundle(format!(
28 "type was '{}'",
29 bundle.type_
30 )));
31 }
32 Ok(bundle)
33 }
34}
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39 use crate::view::ObjectView;
40
41 fn bundle_json() -> &'static str {
42 r#"{
43 "type": "bundle",
44 "id": "bundle--1",
45 "objects": [
46 {"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.2.3.4"},
47 {"type": "observed-data", "id": "observed-data--1",
48 "first_observed": "2020-01-01T00:00:00Z",
49 "last_observed": "2020-01-01T00:00:00Z",
50 "number_observed": 1, "object_refs": ["ipv4-addr--1"]}
51 ]
52 }"#
53 }
54
55 #[test]
56 fn parses_bundle() {
57 let b = Bundle::from_json_str(bundle_json()).unwrap();
58 assert_eq!(b.id.as_deref(), Some("bundle--1"));
59 assert_eq!(b.objects.len(), 2);
60 assert_eq!(b.objects[0].type_(), Some("ipv4-addr"));
61 assert_eq!(b.objects[1].type_(), Some("observed-data"));
62 }
63
64 #[test]
65 fn rejects_non_bundle() {
66 let err = Bundle::from_json_str(r#"{"type": "ipv4-addr", "id": "x--1"}"#).unwrap_err();
67 assert!(matches!(err, crate::error::ModelError::NotABundle(_)));
68 }
69
70 #[test]
71 fn round_trips() {
72 let b = Bundle::from_json_str(bundle_json()).unwrap();
73 let s = serde_json::to_string(&b).unwrap();
74 let b2 = Bundle::from_json_str(&s).unwrap();
75 assert_eq!(b.objects.len(), b2.objects.len());
76 }
77}