Skip to main content

cloudconvert_sdk/
file_extension.rs

1//! CloudConvert file extension tokens and normalization helpers.
2//!
3//! [`FileExtension`] serializes to the lowercase extension string CloudConvert
4//! expects, without a leading dot. The enum is derived from operations metadata
5//! and accepts common user input variants through [`FileExtension::parse`].
6
7use std::{fmt, str::FromStr};
8
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10
11macro_rules! file_extensions {
12    ($($variant:ident => $extension:literal),+ $(,)?) => {
13        /// Supported CloudConvert file extension tokens.
14        ///
15        /// Values serialize to the lowercase extension token CloudConvert expects,
16        /// without a leading dot. The list is derived from CloudConvert's
17        /// operations metadata endpoint.
18        ///
19        /// ```
20        /// use cloudconvert_sdk::FileExtension;
21        ///
22        /// assert_eq!(FileExtension::Pdf.as_str(), "pdf");
23        /// assert_eq!(".TAR.GZ".parse::<FileExtension>().unwrap(), FileExtension::TarGz);
24        /// assert_eq!(
25        ///     serde_json::to_value(FileExtension::SevenZ).unwrap(),
26        ///     serde_json::json!("7z")
27        /// );
28        /// ```
29        #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
30        #[non_exhaustive]
31        pub enum FileExtension {
32            $($variant,)+
33        }
34
35        impl FileExtension {
36            /// All known file extensions in sorted order.
37            pub const ALL: &'static [Self] = &[
38                $(Self::$variant,)+
39            ];
40
41            /// Returns the extension token CloudConvert expects.
42            pub const fn as_str(self) -> &'static str {
43                match self {
44                    $(Self::$variant => $extension,)+
45                }
46            }
47
48            /// Parses a CloudConvert extension token.
49            ///
50            /// Leading dots, surrounding whitespace and ASCII case differences are
51            /// normalized before matching.
52            ///
53            /// ```
54            /// use cloudconvert_sdk::FileExtension;
55            ///
56            /// assert_eq!(FileExtension::parse(" .PDF ").unwrap(), FileExtension::Pdf);
57            /// ```
58            pub fn parse(value: &str) -> Result<Self, ParseFileExtensionError> {
59                value.parse()
60            }
61        }
62
63        impl FromStr for FileExtension {
64            type Err = ParseFileExtensionError;
65
66            fn from_str(value: &str) -> Result<Self, Self::Err> {
67                let normalized = normalize_file_extension(value);
68
69                match normalized.as_str() {
70                    $($extension => Ok(Self::$variant),)+
71                    _ => Err(ParseFileExtensionError::new(value)),
72                }
73            }
74        }
75    };
76}
77
78file_extensions! {
79    ThreeFr => "3fr",
80    ThreeG2 => "3g2",
81    ThreeGp => "3gp",
82    ThreeGpp => "3gpp",
83    SevenZ => "7z",
84    Aac => "aac",
85    Abw => "abw",
86    Ac3 => "ac3",
87    Ace => "ace",
88    Ai => "ai",
89    Aif => "aif",
90    Aifc => "aifc",
91    Aiff => "aiff",
92    Alz => "alz",
93    Amr => "amr",
94    Arc => "arc",
95    Arj => "arj",
96    Arw => "arw",
97    Au => "au",
98    Avi => "avi",
99    Avif => "avif",
100    Azw => "azw",
101    Azw3 => "azw3",
102    Azw4 => "azw4",
103    Bmp => "bmp",
104    Bz => "bz",
105    Bz2 => "bz2",
106    Cab => "cab",
107    Caf => "caf",
108    Cavs => "cavs",
109    Cbc => "cbc",
110    Cbr => "cbr",
111    Cbz => "cbz",
112    Cdr => "cdr",
113    Cgm => "cgm",
114    Chm => "chm",
115    Cpio => "cpio",
116    Cr2 => "cr2",
117    Cr3 => "cr3",
118    Crw => "crw",
119    Csv => "csv",
120    Dcr => "dcr",
121    Deb => "deb",
122    Djvu => "djvu",
123    Dmg => "dmg",
124    Dng => "dng",
125    Doc => "doc",
126    Docm => "docm",
127    Docx => "docx",
128    Dot => "dot",
129    Dotx => "dotx",
130    Dps => "dps",
131    Dss => "dss",
132    Dv => "dv",
133    Dvr => "dvr",
134    Dwf => "dwf",
135    Dwg => "dwg",
136    Dxf => "dxf",
137    Emf => "emf",
138    Eml => "eml",
139    Eot => "eot",
140    Eps => "eps",
141    Epub => "epub",
142    Erf => "erf",
143    Et => "et",
144    Fb2 => "fb2",
145    Flac => "flac",
146    Flv => "flv",
147    Gif => "gif",
148    Gz => "gz",
149    Heic => "heic",
150    Heif => "heif",
151    Htm => "htm",
152    Html => "html",
153    Htmlz => "htmlz",
154    Hwp => "hwp",
155    Hwpx => "hwpx",
156    Icns => "icns",
157    Ico => "ico",
158    Img => "img",
159    Iso => "iso",
160    Jar => "jar",
161    Jfif => "jfif",
162    Jpeg => "jpeg",
163    Jpg => "jpg",
164    Key => "key",
165    Lha => "lha",
166    Lit => "lit",
167    Lrf => "lrf",
168    Lwp => "lwp",
169    Lz => "lz",
170    Lzma => "lzma",
171    Lzo => "lzo",
172    M2ts => "m2ts",
173    M4a => "m4a",
174    M4b => "m4b",
175    M4v => "m4v",
176    Md => "md",
177    Mkv => "mkv",
178    Mobi => "mobi",
179    Mod => "mod",
180    Mos => "mos",
181    Mov => "mov",
182    Mp3 => "mp3",
183    Mp4 => "mp4",
184    Mpeg => "mpeg",
185    Mpg => "mpg",
186    Mrw => "mrw",
187    Mts => "mts",
188    Mxf => "mxf",
189    Nef => "nef",
190    Numbers => "numbers",
191    Odd => "odd",
192    Odg => "odg",
193    Odp => "odp",
194    Ods => "ods",
195    Odt => "odt",
196    Oeb => "oeb",
197    Oga => "oga",
198    Ogg => "ogg",
199    Ogv => "ogv",
200    Opus => "opus",
201    Orf => "orf",
202    Otf => "otf",
203    Pages => "pages",
204    Pdb => "pdb",
205    Pdf => "pdf",
206    Pef => "pef",
207    Pml => "pml",
208    Png => "png",
209    Pot => "pot",
210    Potx => "potx",
211    Ppm => "ppm",
212    Pps => "pps",
213    Ppsx => "ppsx",
214    Ppt => "ppt",
215    Pptm => "pptm",
216    Pptx => "pptx",
217    Prc => "prc",
218    Ps => "ps",
219    Psb => "psb",
220    Psd => "psd",
221    Pub => "pub",
222    Raf => "raf",
223    Rar => "rar",
224    Raw => "raw",
225    Rb => "rb",
226    Rm => "rm",
227    Rmvb => "rmvb",
228    Rpm => "rpm",
229    Rst => "rst",
230    Rtf => "rtf",
231    Rw2 => "rw2",
232    Rz => "rz",
233    Sda => "sda",
234    Sdc => "sdc",
235    Sdw => "sdw",
236    Sf2 => "sf2",
237    Sfark => "sfark",
238    Sk => "sk",
239    Sk1 => "sk1",
240    Snb => "snb",
241    Svg => "svg",
242    Svgz => "svgz",
243    Swf => "swf",
244    Tar => "tar",
245    Tar7z => "tar.7z",
246    TarBz => "tar.bz",
247    TarBz2 => "tar.bz2",
248    TarGz => "tar.gz",
249    TarLzo => "tar.lzo",
250    TarXz => "tar.xz",
251    TarZ => "tar.z",
252    Tbz => "tbz",
253    Tbz2 => "tbz2",
254    Tcr => "tcr",
255    Tex => "tex",
256    Tga => "tga",
257    Tgz => "tgz",
258    Tif => "tif",
259    Tiff => "tiff",
260    Ts => "ts",
261    Ttf => "ttf",
262    Txt => "txt",
263    Txtz => "txtz",
264    Tz => "tz",
265    Tzo => "tzo",
266    Vob => "vob",
267    Voc => "voc",
268    Vsd => "vsd",
269    Vtt => "vtt",
270    Wav => "wav",
271    Weba => "weba",
272    Webm => "webm",
273    Webp => "webp",
274    Wma => "wma",
275    Wmf => "wmf",
276    Wmv => "wmv",
277    Woff => "woff",
278    Woff2 => "woff2",
279    Wpd => "wpd",
280    Wps => "wps",
281    Wtv => "wtv",
282    X3f => "x3f",
283    Xcf => "xcf",
284    Xls => "xls",
285    Xlsm => "xlsm",
286    Xlsx => "xlsx",
287    Xps => "xps",
288    Xz => "xz",
289    Z => "z",
290    Zabw => "zabw",
291    Zip => "zip",
292}
293
294impl AsRef<str> for FileExtension {
295    fn as_ref(&self) -> &str {
296        self.as_str()
297    }
298}
299
300impl fmt::Display for FileExtension {
301    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
302        formatter.write_str(self.as_str())
303    }
304}
305
306impl From<FileExtension> for String {
307    fn from(value: FileExtension) -> Self {
308        value.as_str().to_string()
309    }
310}
311
312impl From<&FileExtension> for String {
313    fn from(value: &FileExtension) -> Self {
314        value.as_str().to_string()
315    }
316}
317
318impl Serialize for FileExtension {
319    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
320    where
321        S: Serializer,
322    {
323        serializer.serialize_str(self.as_str())
324    }
325}
326
327impl<'de> Deserialize<'de> for FileExtension {
328    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
329    where
330        D: Deserializer<'de>,
331    {
332        let value = String::deserialize(deserializer)?;
333        value.parse().map_err(serde::de::Error::custom)
334    }
335}
336
337/// Error returned when parsing a string into [`FileExtension`] fails.
338///
339/// ```
340/// use cloudconvert_sdk::FileExtension;
341///
342/// let error = FileExtension::parse("not-real").unwrap_err();
343/// assert_eq!(error.extension(), "not-real");
344/// ```
345#[derive(Clone, Debug, Eq, PartialEq)]
346pub struct ParseFileExtensionError {
347    extension: String,
348}
349
350impl ParseFileExtensionError {
351    fn new(extension: impl Into<String>) -> Self {
352        Self {
353            extension: extension.into(),
354        }
355    }
356
357    /// The unrecognized extension as provided to the parser.
358    pub fn extension(&self) -> &str {
359        &self.extension
360    }
361}
362
363impl fmt::Display for ParseFileExtensionError {
364    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
365        write!(
366            formatter,
367            "unsupported CloudConvert file extension: {}",
368            self.extension
369        )
370    }
371}
372
373impl std::error::Error for ParseFileExtensionError {}
374
375pub(crate) fn normalize_file_extension(value: impl Into<String>) -> String {
376    value
377        .into()
378        .trim()
379        .trim_start_matches('.')
380        .to_ascii_lowercase()
381}