1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4use serde_with::skip_serializing_none;
5
6use super::common::{CustomField, MetaData, NtRef};
7use crate::{QBCreatable, QBDeletable, QBFullUpdatable, QBItem, QBToRef, QBTypeError};
8
9#[skip_serializing_none]
10#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
11#[serde(rename_all = "PascalCase", default)]
12#[cfg_attr(
13 feature = "builder",
14 derive(Builder),
15 builder(default, build_fn(error = "QBTypeError"), setter(into, strip_option))
16)]
17
18pub struct Attachable {
29 pub id: Option<String>,
31 pub sync_token: Option<String>,
33 #[serde(skip_serializing)]
35 pub meta_data: Option<MetaData>,
36 #[cfg_attr(feature = "builder", builder(setter(custom)))]
38 pub file_name: Option<String>,
39 #[cfg_attr(feature = "builder", builder(setter(custom)))]
41 #[serde(skip)]
42 pub file_path: Option<PathBuf>,
43 pub note: Option<String>,
45 pub category: Option<AttachmentCategory>,
47 #[cfg_attr(feature = "builder", builder(setter(custom)))]
49 pub content_type: Option<String>,
50 pub place_name: Option<String>,
51 #[cfg_attr(feature = "builder", builder(setter(strip_option)))]
53 pub attachable_ref: Option<Vec<AttachableRef>>,
54 pub long: Option<String>,
56 pub tag: Option<String>,
58 pub lat: Option<String>,
60 pub file_access_uri: Option<String>,
62 pub size: Option<f64>,
64 pub thumbnail_file_access_uri: Option<String>,
66 pub temp_download_uri: Option<String>,
68}
69
70#[must_use]
72pub fn content_type_from_ext(ext: &str) -> Option<&'static str> {
73 let out = match ext {
74 "ai" | "eps" => "application/postscript",
75 "csv" => "text/csv",
76 "doc" => "application/msword",
77 "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
78 "gif" => "image/gif",
79 "jpeg" => "image/jpeg",
80 "jpg" => "image/jpg",
81 "png" => "image/png",
82 "rtf" => "text/rtf",
83 "txt" => "text/plain",
84 "tif" => "image/tiff",
85 "ods" => "application/vnd.oasis.opendocument.spreadsheet",
86 "pdf" => "application/pdf",
87 "xls" => "application/vnd.ms-excel",
88 "xml" => "text/xml",
89 "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
90 _ => return None,
91 };
92 Some(out)
93}
94
95#[cfg(feature = "builder")]
96impl AttachableBuilder {
97 pub fn file(&mut self, path: &impl AsRef<Path>) -> Result<&mut Self, QBTypeError> {
100 let path = path.as_ref();
101
102 self.file_path = Some(Some(path.to_path_buf()));
103
104 self.file_name = Some(Some(
105 path.file_name()
106 .ok_or(QBTypeError::ValidationError(
107 "No file name on file path".into(),
108 ))?
109 .to_str()
110 .ok_or(QBTypeError::ValidationError(
111 "Could not turn file name into string".into(),
112 ))?
113 .to_owned(),
114 ));
115
116 self.content_type = Some(
117 content_type_from_ext(
118 path.extension()
119 .ok_or(QBTypeError::ValidationError(
120 "No extension on file/dir".into(),
121 ))?
122 .to_string_lossy()
123 .as_ref(),
124 )
125 .map(|f| f.to_owned()),
126 );
127
128 Ok(self)
129 }
130}
131
132pub trait QBAttachable {
140 fn can_upload(&self) -> Result<(), QBTypeError>;
142 fn file_path(&self) -> Option<&Path>;
143}
144impl QBAttachable for Attachable {
145 fn can_upload(&self) -> Result<(), QBTypeError> {
146 if self.file_name.is_none() {
147 return Err(QBTypeError::MissingField("file_name"));
148 }
149 if self.file_path.is_none() {
150 return Err(QBTypeError::MissingField("file_path"));
151 }
152 if self.content_type.is_none() {
153 return Err(QBTypeError::MissingField("content_type"));
154 }
155 Ok(())
156 }
157
158 fn file_path(&self) -> Option<&Path> {
159 self.file_path.as_deref()
160 }
161}
162
163#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
167pub enum AttachmentCategory {
168 ContactPhoto,
169 Document,
170 Image,
171 Receipt,
172 Signature,
173 Sound,
174 #[default]
175 Other,
176}
177
178#[skip_serializing_none]
184#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
185#[serde(rename_all = "PascalCase", default)]
186#[cfg_attr(feature = "builder", derive(Builder), builder(default))]
187pub struct AttachableRef {
188 pub include_on_send: Option<bool>,
190 pub line_info: Option<String>,
192 pub no_ref_only: Option<bool>,
194 pub custom_field: Option<Vec<CustomField>>,
196 #[serde(rename = "type")]
198 pub ref_type: Option<String>,
199 pub inactive: Option<bool>,
201 pub entity_ref: Option<NtRef>,
203}
204
205impl From<NtRef> for AttachableRef {
206 fn from(value: NtRef) -> Self {
207 AttachableRef {
208 entity_ref: Some(value),
209 ..Default::default()
210 }
211 }
212}
213
214pub trait QBToAttachableRef: QBToRef {
216 fn to_attach_ref(&self) -> Result<AttachableRef, QBTypeError> {
217 let value = self.to_ref()?;
218 Ok(value.into())
219 }
220}
221
222impl<T: QBToRef> QBToAttachableRef for T {}
223
224impl QBCreatable for Attachable {
225 fn can_create(&self) -> bool {
226 (self.file_name.is_some() || self.note.is_some())
227 && self.content_type.is_some()
228 && self.file_path.is_some()
229 }
230}
231impl QBDeletable for Attachable {}
232impl QBFullUpdatable for Attachable {
233 fn can_full_update(&self) -> bool {
234 self.has_read() && self.can_create()
235 }
236}