Skip to main content

systemprompt_provider_contracts/documents/
format.rs

1//! The document formats the store understands.
2//!
3//! [`DocumentFormat::File`] is any other upload (a PDF, an image, an
4//! archive): stored and served as bytes, listed like any document, never
5//! projected or edited. It is never inferred from an extension except its
6//! own `bin`; an import falls back to it when no other format fits.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use std::fmt;
12use std::str::FromStr;
13
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16
17#[derive(
18    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
19)]
20#[serde(rename_all = "snake_case")]
21pub enum DocumentFormat {
22    Docx,
23    Xlsx,
24    Pptx,
25    Odt,
26    Ods,
27    Odp,
28    Html,
29    Markdown,
30    Text,
31    File,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
35#[error("unknown document format `{value}`")]
36pub struct UnknownDocumentFormat {
37    pub value: String,
38}
39
40impl DocumentFormat {
41    pub const ALL: [Self; 10] = [
42        Self::Docx,
43        Self::Xlsx,
44        Self::Pptx,
45        Self::Odt,
46        Self::Ods,
47        Self::Odp,
48        Self::Html,
49        Self::Markdown,
50        Self::Text,
51        Self::File,
52    ];
53
54    #[must_use]
55    pub const fn as_str(self) -> &'static str {
56        match self {
57            Self::Docx => "docx",
58            Self::Xlsx => "xlsx",
59            Self::Pptx => "pptx",
60            Self::Odt => "odt",
61            Self::Ods => "ods",
62            Self::Odp => "odp",
63            Self::Html => "html",
64            Self::Markdown => "markdown",
65            Self::Text => "text",
66            Self::File => "file",
67        }
68    }
69
70    #[must_use]
71    pub const fn mime_type(self) -> &'static str {
72        match self {
73            Self::Docx => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
74            Self::Xlsx => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
75            Self::Pptx => {
76                "application/vnd.openxmlformats-officedocument.presentationml.presentation"
77            },
78            Self::Odt => "application/vnd.oasis.opendocument.text",
79            Self::Ods => "application/vnd.oasis.opendocument.spreadsheet",
80            Self::Odp => "application/vnd.oasis.opendocument.presentation",
81            Self::Html => "text/html",
82            Self::Markdown => "text/markdown",
83            Self::Text => "text/plain",
84            Self::File => "application/octet-stream",
85        }
86    }
87
88    #[must_use]
89    pub const fn extension(self) -> &'static str {
90        match self {
91            Self::Markdown => "md",
92            Self::Text => "txt",
93            Self::File => "bin",
94            other => other.as_str(),
95        }
96    }
97
98    #[must_use]
99    pub fn from_extension(extension: &str) -> Option<Self> {
100        let lower = extension.trim_start_matches('.').to_ascii_lowercase();
101        match lower.as_str() {
102            "htm" => Some(Self::Html),
103            "md" | "markdown" => Some(Self::Markdown),
104            "txt" | "text" | "csv" => Some(Self::Text),
105            "bin" => Some(Self::File),
106            "file" => None,
107            other => Self::ALL.into_iter().find(|f| f.as_str() == other),
108        }
109    }
110
111    #[must_use]
112    pub const fn is_office(self) -> bool {
113        matches!(
114            self,
115            Self::Docx | Self::Xlsx | Self::Pptx | Self::Odt | Self::Ods | Self::Odp
116        )
117    }
118
119    #[must_use]
120    pub const fn is_file(self) -> bool {
121        matches!(self, Self::File)
122    }
123
124    #[must_use]
125    pub const fn supports_three_way_merge(self) -> bool {
126        matches!(self, Self::Html | Self::Markdown | Self::Text)
127    }
128}
129
130impl fmt::Display for DocumentFormat {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        f.write_str(self.as_str())
133    }
134}
135
136impl FromStr for DocumentFormat {
137    type Err = UnknownDocumentFormat;
138
139    fn from_str(s: &str) -> Result<Self, Self::Err> {
140        Self::ALL
141            .into_iter()
142            .find(|f| f.as_str() == s)
143            .ok_or_else(|| UnknownDocumentFormat {
144                value: s.to_owned(),
145            })
146    }
147}