Skip to main content

virustotal_rs/
objects.rs

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    /// Check if iteration has finished
189    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}