Skip to main content

twilight_http/request/
attachment.rs

1use crate::request::Form;
2use serde::{Deserialize, Serialize};
3use twilight_model::{
4    http::attachment::Attachment,
5    id::{Id, marker::AttachmentMarker},
6};
7
8pub struct AttachmentManager<'a> {
9    files: Vec<&'a Attachment>,
10    ids: Vec<Id<AttachmentMarker>>,
11}
12
13impl<'a> AttachmentManager<'a> {
14    pub const fn new() -> Self {
15        Self {
16            files: Vec::new(),
17            ids: Vec::new(),
18        }
19    }
20
21    pub fn build_form(&self, fields: &'a [u8]) -> Form {
22        let mut form = Form::new().json_part(b"payload_json", fields);
23
24        for file in &self.files {
25            let mut name = Vec::with_capacity(7 + num_digits(file.id));
26            name.extend(b"files[");
27            push_digits(file.id, &mut name);
28            name.extend(b"]");
29
30            form = form.file_part(name.as_ref(), file.filename.as_bytes(), file.file.as_ref());
31        }
32
33        form
34    }
35
36    pub fn get_partial_attachments(&self) -> Vec<PartialAttachment<'a>> {
37        self.files
38            .iter()
39            .map(|attachment| PartialAttachment {
40                title: attachment.title.as_deref(),
41                description: attachment.description.as_deref(),
42                filename: Some(attachment.filename.as_ref()),
43                id: attachment.id,
44                duration_secs: attachment.duration_secs,
45                waveform: attachment.waveform.as_deref(),
46                is_spoiler: attachment.is_spoiler,
47            })
48            .chain(self.ids.iter().map(|id| PartialAttachment {
49                title: None,
50                description: None,
51                filename: None,
52                id: id.get(),
53                duration_secs: None,
54                waveform: None,
55                is_spoiler: None,
56            }))
57            .collect()
58    }
59
60    pub const fn is_empty(&self) -> bool {
61        self.files.is_empty() && self.ids.is_empty()
62    }
63
64    #[must_use = "has no effect if not built into a Form"]
65    pub fn set_files(mut self, files: Vec<&'a Attachment>) -> Self {
66        self.files = files;
67
68        self
69    }
70
71    #[must_use = "has no effect if not built into a Form"]
72    pub fn set_ids(mut self, ids: Vec<Id<AttachmentMarker>>) -> Self {
73        self.ids = ids;
74
75        self
76    }
77}
78
79impl Default for AttachmentManager<'_> {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
86pub struct PartialAttachment<'a> {
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub title: Option<&'a str>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub description: Option<&'a str>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub filename: Option<&'a str>,
93    pub id: u64,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub duration_secs: Option<f64>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub waveform: Option<&'a str>,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub is_spoiler: Option<bool>,
100}
101
102/// Count the number of digits in a given number.
103const fn num_digits(index: u64) -> usize {
104    let mut index = index;
105    let mut len = 0;
106
107    if index < 10 {
108        return 1;
109    }
110
111    while index > 0 {
112        index /= 10;
113        len += 1;
114    }
115
116    len
117}
118
119/// Value of '0' in ascii
120const ASCII_NUMBER: u8 = 0x30;
121
122/// Extend the buffer with the digits of the integer `id`.
123///
124/// The reason for this is to get around a allocation by for example using
125/// `format!("files[{id}]")`.
126fn push_digits(mut id: u64, buf: &mut Vec<u8>) {
127    // The largest 64 bit integer is 20 digits.
128    let mut inner_buf = [0_u8; 20];
129    // Amount of digits written to the inner buffer.
130    let mut i = 0;
131
132    // While the number have more than one digit we print the last digit by
133    // taking the rest after modulo 10. We then divide with 10 to truncate the
134    // number from the right and then loop
135    while id >= 10 {
136        // To go from the integer to the ascii value we add the ascii value of
137        // '0'.
138        //
139        // (id % 10) will always be less than 10 so truncation cannot happen.
140        #[allow(clippy::cast_possible_truncation)]
141        let ascii = (id % 10) as u8 + ASCII_NUMBER;
142        inner_buf[i] = ascii;
143        id /= 10;
144        i += 1;
145    }
146    // (id % 10) will always be less than 10 so truncation cannot happen.
147    #[allow(clippy::cast_possible_truncation)]
148    let ascii = (id % 10) as u8 + ASCII_NUMBER;
149    inner_buf[i] = ascii;
150    i += 1;
151
152    // As we have written the digits in reverse we reverse the area of the array
153    // we have been using to get the characters in the correct order.
154    inner_buf[..i].reverse();
155
156    buf.extend_from_slice(&inner_buf[..i]);
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn push_digits_limits() {
165        let min_d = b"0";
166        let max_d = b"18446744073709551615";
167
168        let mut min_v = Vec::new();
169        let mut max_v = Vec::new();
170
171        push_digits(u64::MIN, &mut min_v);
172        push_digits(u64::MAX, &mut max_v);
173
174        assert_eq!(min_d[..], min_v[..]);
175        assert_eq!(max_d[..], max_v[..]);
176    }
177
178    #[test]
179    fn num_digits_count() {
180        assert_eq!(1, num_digits(0));
181        assert_eq!(1, num_digits(1));
182        assert_eq!(2, num_digits(10));
183    }
184}