1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::marker::PhantomData;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Object<T> {
7 #[serde(rename = "type")]
8 pub object_type: String,
9 pub id: String,
10 pub links: Option<Links>,
11 pub attributes: T,
12 #[serde(skip_serializing_if = "Option::is_none")]
13 pub relationships: Option<HashMap<String, Relationship>>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Links {
18 #[serde(rename = "self")]
19 pub self_link: String,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub next: Option<String>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub related: Option<String>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(untagged)]
28pub enum Relationship {
29 OneToOne(OneToOneRelationship),
30 OneToMany(OneToManyRelationship),
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct OneToOneRelationship {
35 pub data: ObjectDescriptor,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub links: Option<Links>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct OneToManyRelationship {
42 pub data: Vec<ObjectDescriptorOrError>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub meta: Option<CollectionMeta>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub links: Option<Links>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ObjectDescriptor {
51 #[serde(rename = "type")]
52 pub object_type: String,
53 pub id: String,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(untagged)]
58pub enum ObjectDescriptorOrError {
59 Descriptor(ObjectDescriptor),
60 Error {
61 error: RelationshipError,
62 id: String,
63 #[serde(rename = "type")]
64 object_type: String,
65 },
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct RelationshipError {
70 pub code: String,
71 pub message: String,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ObjectResponse<T> {
76 pub data: Object<T>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct PatchRequest<T> {
81 pub data: PatchData<T>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct PatchData<T> {
86 #[serde(rename = "type")]
87 pub object_type: String,
88 pub id: String,
89 pub attributes: T,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct Collection<T> {
94 pub data: Vec<T>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub meta: Option<CollectionMeta>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub links: Option<Links>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct CollectionMeta {
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub cursor: Option<String>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub count: Option<u64>,
107}
108
109pub struct CollectionIterator<'a, T> {
110 client: &'a crate::Client,
111 url: String,
112 cursor: Option<String>,
113 finished: bool,
114 limit: Option<u32>,
115 _phantom: PhantomData<T>,
116}
117
118impl<'a, T> CollectionIterator<'a, T>
119where
120 T: for<'de> Deserialize<'de> + Clone,
121{
122 pub fn new(client: &'a crate::Client, url: impl Into<String>) -> Self {
123 Self {
124 client,
125 url: url.into(),
126 cursor: None,
127 finished: false,
128 limit: None,
129 _phantom: PhantomData,
130 }
131 }
132
133 pub fn with_limit(mut self, limit: u32) -> Self {
134 self.limit = Some(limit);
135 self
136 }
137
138 pub async fn next_batch(&mut self) -> crate::Result<Vec<T>> {
139 if self.finished {
140 return Ok(Vec::new());
141 }
142
143 let mut url = self.url.clone();
144 let mut query_params = Vec::new();
145
146 if let Some(cursor) = &self.cursor {
147 query_params.push(format!("cursor={}", cursor));
148 }
149
150 if let Some(limit) = self.limit {
151 query_params.push(format!("limit={}", limit));
152 }
153
154 if !query_params.is_empty() {
155 url = format!("{}?{}", url, query_params.join("&"));
156 }
157
158 let response: Collection<T> = self.client.get(&url).await?;
159
160 let items = response.data;
161
162 if let Some(meta) = response.meta {
163 self.cursor = meta.cursor;
164 if self.cursor.is_none() {
165 self.finished = true;
166 }
167 } else {
168 self.finished = true;
169 }
170
171 Ok(items)
172 }
173
174 pub async fn collect_all(mut self) -> crate::Result<Vec<T>> {
175 let mut all_items = Vec::new();
176
177 while !self.finished {
178 let batch = self.next_batch().await?;
179 if batch.is_empty() {
180 break;
181 }
182 all_items.extend(batch);
183 }
184
185 Ok(all_items)
186 }
187
188 pub fn is_finished(&self) -> bool {
190 self.finished
191 }
192}
193
194pub trait ObjectOperations {
195 type Attributes: Serialize + for<'de> Deserialize<'de>;
196
197 fn collection_name() -> &'static str;
198
199 fn object_url(id: &str) -> String {
200 format!("{}/{}", Self::collection_name(), id)
201 }
202
203 fn relationships_url(id: &str, relationship: &str) -> String {
204 format!(
205 "{}/{}/relationships/{}",
206 Self::collection_name(),
207 id,
208 relationship
209 )
210 }
211
212 fn relationship_objects_url(id: &str, relationship: &str) -> String {
213 format!("{}/{}/{}", Self::collection_name(), id, relationship)
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use serde_json::json;
221
222 #[test]
223 fn test_object_serialization() {
224 let obj = Object {
225 object_type: "file".to_string(),
226 id: "abc123".to_string(),
227 links: Some(Links {
228 self_link: "https://www.virustotal.com/api/v3/files/abc123".to_string(),
229 next: None,
230 related: None,
231 }),
232 attributes: json!({"name": "test.exe", "size": 1024}),
233 relationships: None,
234 };
235
236 let json = serde_json::to_string(&obj).unwrap();
237 assert!(json.contains("\"type\":\"file\""));
238 assert!(json.contains("\"id\":\"abc123\""));
239 }
240
241 #[test]
242 fn test_collection_deserialization() {
243 let json = r#"{
244 "data": [
245 {"type": "file", "id": "1"},
246 {"type": "file", "id": "2"}
247 ],
248 "meta": {
249 "cursor": "next_cursor"
250 }
251 }"#;
252
253 let collection: Collection<ObjectDescriptor> = serde_json::from_str(json).unwrap();
254 assert_eq!(collection.data.len(), 2);
255 assert_eq!(collection.meta.unwrap().cursor.unwrap(), "next_cursor");
256 }
257}