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