1use super::TileType;
30use anyhow::{Result, bail};
31#[cfg(feature = "cli")]
32use clap::ValueEnum;
33use std::{
34 fmt::{Display, Formatter},
35 path::Path,
36};
37
38#[allow(clippy::upper_case_acronyms)]
56#[cfg_attr(feature = "cli", derive(ValueEnum))]
57#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
58pub enum TileFormat {
59 AVIF,
60 #[default]
61 BIN,
62 GEOJSON,
63 JPG,
64 JSON,
65 MVT,
66 PNG,
67 SVG,
68 TOPOJSON,
69 WEBP,
70}
71
72impl TileFormat {
73 #[must_use]
82 pub fn as_str(&self) -> &str {
83 match self {
84 TileFormat::AVIF => "avif",
85 TileFormat::BIN => "bin",
86 TileFormat::GEOJSON => "geojson",
87 TileFormat::JPG => "jpg",
88 TileFormat::JSON => "json",
89 TileFormat::MVT => "mvt",
90 TileFormat::PNG => "png",
91 TileFormat::SVG => "svg",
92 TileFormat::TOPOJSON => "topojson",
93 TileFormat::WEBP => "webp",
94 }
95 }
96
97 pub fn try_from_str(value: &str) -> Result<Self> {
98 Ok(match value.to_lowercase().trim() {
99 "avif" => TileFormat::AVIF,
100 "bin" => TileFormat::BIN,
101 "geojson" => TileFormat::GEOJSON,
102 "jpeg" | "jpg" => TileFormat::JPG,
103 "json" => TileFormat::JSON,
104 "pbf" | "mvt" => TileFormat::MVT,
105 "png" => TileFormat::PNG,
106 "svg" => TileFormat::SVG,
107 "topojson" => TileFormat::TOPOJSON,
108 "webp" => TileFormat::WEBP,
109 _ => bail!("Unknown tile format: '{value}'"),
110 })
111 }
112
113 pub fn try_from_path(path: &Path) -> Result<Self> {
114 Self::try_from_str(path.extension().and_then(|s| s.to_str()).unwrap_or_default())
115 }
116
117 #[must_use]
128 pub fn as_type_str(&self) -> &str {
129 match self {
130 TileFormat::AVIF | TileFormat::JPG | TileFormat::PNG | TileFormat::SVG | TileFormat::WEBP => "image",
131 TileFormat::BIN | TileFormat::JSON => "unknown",
132 TileFormat::GEOJSON | TileFormat::MVT | TileFormat::TOPOJSON => "vector",
133 }
134 }
135
136 #[must_use]
147 pub fn as_mime_str(&self) -> &str {
148 match self {
149 TileFormat::BIN => "application/octet-stream",
150 TileFormat::PNG => "image/png",
151 TileFormat::JPG => "image/jpeg",
152 TileFormat::WEBP => "image/webp",
153 TileFormat::AVIF => "image/avif",
154 TileFormat::SVG => "image/svg+xml",
155 TileFormat::MVT => "vnd.mapbox-vector-tile",
156 TileFormat::GEOJSON => "application/geo+json",
157 TileFormat::TOPOJSON => "application/topo+json",
158 TileFormat::JSON => "application/json",
159 }
160 }
161
162 pub fn try_from_mime(mime: &str) -> Result<Self> {
163 Ok(match mime.to_lowercase().as_str() {
164 "application/octet-stream" => TileFormat::BIN,
165 "image/png" => TileFormat::PNG,
166 "image/jpeg" => TileFormat::JPG,
167 "image/webp" => TileFormat::WEBP,
168 "image/avif" => TileFormat::AVIF,
169 "image/svg+xml" => TileFormat::SVG,
170 "vnd.mapbox-vector-tile" => TileFormat::MVT,
171 "application/geo+json" => TileFormat::GEOJSON,
172 "application/topo+json" => TileFormat::TOPOJSON,
173 "application/json" => TileFormat::JSON,
174 _ => bail!("Unknown MIME type: '{mime}'"),
175 })
176 }
177
178 #[must_use]
187 pub fn as_extension(&self) -> &str {
188 match self {
189 TileFormat::AVIF => ".avif",
190 TileFormat::BIN => ".bin",
191 TileFormat::GEOJSON => ".geojson",
192 TileFormat::JPG => ".jpg",
193 TileFormat::JSON => ".json",
194 TileFormat::MVT => ".pbf",
195 TileFormat::PNG => ".png",
196 TileFormat::SVG => ".svg",
197 TileFormat::TOPOJSON => ".topojson",
198 TileFormat::WEBP => ".webp",
199 }
200 }
201
202 pub fn from_filename(filename: &mut String) -> Option<Self> {
228 if let Some(index) = filename.rfind('.') {
229 let extension = filename[index..].to_lowercase();
230 let format = match extension.as_str() {
231 ".avif" => TileFormat::AVIF,
232 ".bin" => TileFormat::BIN,
233 ".geojson" => TileFormat::GEOJSON,
234 ".jpg" | ".jpeg" => TileFormat::JPG,
235 ".json" => TileFormat::JSON,
236 ".pbf" => TileFormat::MVT,
237 ".png" => TileFormat::PNG,
238 ".svg" => TileFormat::SVG,
239 ".topojson" => TileFormat::TOPOJSON,
240 ".webp" => TileFormat::WEBP,
241 _ => return None,
242 };
243 filename.truncate(index);
244 Some(format)
245 } else {
246 None
247 }
248 }
249
250 pub fn parse_str(value: &str) -> Result<Self> {
274 Ok(match value.to_lowercase().trim_matches([' ', '.']) {
275 "avif" => TileFormat::AVIF,
276 "bin" => TileFormat::BIN,
277 "geojson" => TileFormat::GEOJSON,
278 "jpeg" | "jpg" => TileFormat::JPG,
279 "json" => TileFormat::JSON,
280 "mvt" => TileFormat::MVT,
281 "png" => TileFormat::PNG,
282 "svg" => TileFormat::SVG,
283 "topojson" => TileFormat::TOPOJSON,
284 "webp" => TileFormat::WEBP,
285 _ => bail!("Unknown tile format: '{}'", value.trim()),
286 })
287 }
288
289 #[must_use]
290 pub fn get_type(&self) -> TileType {
291 use TileFormat::*;
292 use TileType::*;
293 match self {
294 AVIF | PNG | JPG | WEBP => Raster,
295 MVT => Vector,
296 BIN | GEOJSON | JSON | SVG | TOPOJSON => Unknown,
297 }
298 }
299}
300
301impl TryFrom<&str> for TileFormat {
302 type Error = anyhow::Error;
303
304 fn try_from(value: &str) -> Result<Self> {
305 Self::try_from_str(value)
306 }
307}
308
309impl Display for TileFormat {
310 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
311 f.write_str(self.as_str())
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn should_return_correct_extension_for_format() {
321 #[rustfmt::skip]
322 let cases = vec![
323 (TileFormat::AVIF, ".avif"),
324 (TileFormat::BIN, ".bin"),
325 (TileFormat::GEOJSON, ".geojson"),
326 (TileFormat::JPG, ".jpg"),
327 (TileFormat::JSON, ".json"),
328 (TileFormat::MVT, ".pbf"),
329 (TileFormat::PNG, ".png"),
330 (TileFormat::SVG, ".svg"),
331 (TileFormat::TOPOJSON, ".topojson"),
332 (TileFormat::WEBP, ".webp"),
333 ];
334
335 for (format, expected) in cases {
336 assert_eq!(format.as_extension(), expected);
337 }
338 }
339
340 #[test]
341 fn should_extract_correct_format_and_truncate_filename_when_extension_found() {
342 struct Case(&'static str, Option<TileFormat>, &'static str);
343
344 let cases = vec![
345 Case("image.avif", Some(TileFormat::AVIF), "image"),
346 Case("archive.zip", None, "archive.zip"),
347 Case("binary.bin", Some(TileFormat::BIN), "binary"),
348 Case("noextensionfile", None, "noextensionfile"),
349 Case("unknown.ext", None, "unknown.ext"),
350 Case("data.geojson", Some(TileFormat::GEOJSON), "data"),
351 Case("image.jpeg", Some(TileFormat::JPG), "image"),
352 Case("image.jpg", Some(TileFormat::JPG), "image"),
353 Case("document.json", Some(TileFormat::JSON), "document"),
354 Case("map.pbf", Some(TileFormat::MVT), "map"),
355 Case("picture.png", Some(TileFormat::PNG), "picture"),
356 Case("diagram.svg", Some(TileFormat::SVG), "diagram"),
357 Case("vector.SVG", Some(TileFormat::SVG), "vector"),
358 Case("topography.topojson", Some(TileFormat::TOPOJSON), "topography"),
359 Case("photo.webp", Some(TileFormat::WEBP), "photo"),
360 ];
361
362 for case in cases {
363 let mut filename = String::from(case.0);
364 let format = TileFormat::from_filename(&mut filename);
365 assert_eq!(format, case.1);
366 assert_eq!(filename, case.2);
367 }
368 }
369
370 #[test]
371 fn should_parse_str_into_tileformat() {
372 struct Case(&'static str, Option<TileFormat>);
373
374 let cases = vec![
375 Case("avif", Some(TileFormat::AVIF)),
376 Case(".bin", Some(TileFormat::BIN)),
377 Case("GEOJSON", Some(TileFormat::GEOJSON)),
378 Case("jpeg", Some(TileFormat::JPG)),
379 Case("jpg", Some(TileFormat::JPG)),
380 Case(".json", Some(TileFormat::JSON)),
381 Case(" mvt ", Some(TileFormat::MVT)),
382 Case("png", Some(TileFormat::PNG)),
383 Case(".topojson", Some(TileFormat::TOPOJSON)),
384 Case(".webp", Some(TileFormat::WEBP)),
385 Case("unknown", None),
386 ];
387
388 for case in cases {
389 let result = TileFormat::parse_str(case.0);
390 match case.1 {
391 Some(expected_format) => {
392 assert_eq!(result.unwrap(), expected_format);
393 }
394 None => {
395 assert!(result.is_err());
396 }
397 }
398 }
399 }
400
401 #[test]
402 fn should_provide_meaningful_strings_for_debug_and_display() {
403 let format = TileFormat::PNG;
404 assert!(format!("{format:?}").contains("PNG"));
405 assert_eq!(format!("{format}"), "png");
406 }
407
408 #[test]
409 fn should_return_lowercase_string_for_as_str() {
410 #![allow(clippy::enum_variant_names)]
411 #[rustfmt::skip]
412 let cases = vec![
413 (TileFormat::AVIF, "avif"),
414 (TileFormat::BIN, "bin"),
415 (TileFormat::GEOJSON, "geojson"),
416 (TileFormat::JPG, "jpg"),
417 (TileFormat::JSON, "json"),
418 (TileFormat::MVT, "mvt"),
419 (TileFormat::PNG, "png"),
420 (TileFormat::SVG, "svg"),
421 (TileFormat::TOPOJSON, "topojson"),
422 (TileFormat::WEBP, "webp"),
423 ];
424 for (format, expected) in cases {
425 assert_eq!(format.as_str(), expected);
426 }
427 }
428
429 #[test]
430 fn should_return_correct_type_str() {
431 assert_eq!(TileFormat::PNG.as_type_str(), "image");
432 assert_eq!(TileFormat::MVT.as_type_str(), "vector");
433 assert_eq!(TileFormat::BIN.as_type_str(), "unknown");
434 }
435
436 #[test]
437 fn should_return_correct_mime_str() {
438 assert_eq!(TileFormat::PNG.as_mime_str(), "image/png");
439 assert_eq!(TileFormat::JPG.as_mime_str(), "image/jpeg");
440 assert_eq!(TileFormat::GEOJSON.as_mime_str(), "application/geo+json");
441 }
442
443 #[test]
444 fn should_try_from_str_parse_valid_and_error_invalid() {
445 assert_eq!(TileFormat::try_from_str("png").unwrap(), TileFormat::PNG);
446 assert!(TileFormat::try_from_str("invalid").is_err());
447 }
448
449 #[test]
450 fn should_try_from_mime_parse_valid_and_error_invalid() {
451 assert_eq!(TileFormat::try_from_mime("image/webp").unwrap(), TileFormat::WEBP);
452 assert!(TileFormat::try_from_mime("application/x-unknown").is_err());
453 }
454
455 #[test]
456 fn should_get_type_return_expected() {
457 use super::TileType::*;
458 assert_eq!(TileFormat::PNG.get_type(), Raster);
459 assert_eq!(TileFormat::MVT.get_type(), Vector);
460 assert_eq!(TileFormat::BIN.get_type(), Unknown);
461 }
462}