1mod detect;
2mod validate;
3
4use serde::{Deserialize, Serialize};
5
6pub use detect::detect_content_type;
7pub use validate::validate_content;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum ContentType {
12 Url,
13 Json,
14 Yaml,
15 Xml,
16 Markdown,
17 Text,
18 Unknown,
19}
20
21impl ContentType {
22 pub const fn as_str(self) -> &'static str {
23 match self {
24 Self::Url => "url",
25 Self::Json => "json",
26 Self::Yaml => "yaml",
27 Self::Xml => "xml",
28 Self::Markdown => "markdown",
29 Self::Text => "text",
30 Self::Unknown => "unknown",
31 }
32 }
33
34 pub fn parse(value: &str) -> Option<Self> {
35 match value {
36 "url" => Some(Self::Url),
37 "json" => Some(Self::Json),
38 "yaml" => Some(Self::Yaml),
39 "xml" => Some(Self::Xml),
40 "markdown" => Some(Self::Markdown),
41 "text" => Some(Self::Text),
42 "unknown" => Some(Self::Unknown),
43 _ => None,
44 }
45 }
46}
47
48impl std::fmt::Display for ContentType {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.write_str(self.as_str())
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum ValidationStatus {
57 Valid,
58 Invalid,
59 Unknown,
60 Skipped,
61}
62
63impl ValidationStatus {
64 pub const fn as_str(self) -> &'static str {
65 match self {
66 Self::Valid => "valid",
67 Self::Invalid => "invalid",
68 Self::Unknown => "unknown",
69 Self::Skipped => "skipped",
70 }
71 }
72
73 pub fn parse(value: &str) -> Option<Self> {
74 match value {
75 "valid" => Some(Self::Valid),
76 "invalid" => Some(Self::Invalid),
77 "unknown" => Some(Self::Unknown),
78 "skipped" => Some(Self::Skipped),
79 _ => None,
80 }
81 }
82}
83
84impl std::fmt::Display for ValidationStatus {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.write_str(self.as_str())
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct ValidationError {
92 pub code: String,
93 pub message: String,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub path: Option<String>,
96}
97
98impl ValidationError {
99 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
100 Self {
101 code: code.into(),
102 message: message.into(),
103 path: None,
104 }
105 }
106
107 pub fn with_path(mut self, path: impl Into<String>) -> Self {
108 self.path = Some(path.into());
109 self
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114pub struct ValidationResult {
115 pub status: ValidationStatus,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub errors: Option<Vec<ValidationError>>,
118}
119
120impl ValidationResult {
121 pub fn valid() -> Self {
122 Self {
123 status: ValidationStatus::Valid,
124 errors: None,
125 }
126 }
127
128 pub fn invalid(errors: Vec<ValidationError>) -> Self {
129 Self {
130 status: ValidationStatus::Invalid,
131 errors: Some(errors),
132 }
133 }
134
135 pub fn unknown() -> Self {
136 Self {
137 status: ValidationStatus::Unknown,
138 errors: None,
139 }
140 }
141
142 pub fn skipped() -> Self {
143 Self {
144 status: ValidationStatus::Skipped,
145 errors: None,
146 }
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151pub struct AnalysisMetadata {
152 pub content_type: ContentType,
153 pub validation: ValidationResult,
154}
155
156pub fn analyze(input: &str) -> AnalysisMetadata {
157 let content_type = detect_content_type(input);
158 let validation = validate_content(content_type, input);
159 AnalysisMetadata {
160 content_type,
161 validation,
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::{ContentType, ValidationStatus, analyze};
168
169 fn assert_invalid(input: &str, expected_type: ContentType, expected_code: &str) {
170 let result = analyze(input);
171 assert_eq!(result.content_type, expected_type);
172 assert_eq!(result.validation.status, ValidationStatus::Invalid);
173 let errors = result
174 .validation
175 .errors
176 .as_ref()
177 .expect("invalid result must include errors");
178 assert_eq!(errors[0].code, expected_code);
179 }
180
181 #[test]
182 fn analyze_valid_url() {
183 let result = analyze("https://example.com/docs?q=1");
184 assert_eq!(result.content_type, ContentType::Url);
185 assert_eq!(result.validation.status, ValidationStatus::Valid);
186 assert!(result.validation.errors.is_none());
187 }
188
189 #[test]
190 fn analyze_invalid_url() {
191 assert_invalid("https://", ContentType::Url, "invalid-url");
192 }
193
194 #[test]
195 fn analyze_detects_json_before_yaml() {
196 let input = r#"{"name":"memo","priority":1}"#;
197 let result = analyze(input);
198 assert_eq!(result.content_type, ContentType::Json);
199 assert_eq!(result.validation.status, ValidationStatus::Valid);
200 }
201
202 #[test]
203 fn analyze_invalid_json() {
204 assert_invalid(r#"{"name":"memo""#, ContentType::Json, "invalid-json");
205 }
206
207 #[test]
208 fn analyze_valid_yaml() {
209 let input = "name: memo\npriority: high";
210 let result = analyze(input);
211 assert_eq!(result.content_type, ContentType::Yaml);
212 assert_eq!(result.validation.status, ValidationStatus::Valid);
213 }
214
215 #[test]
216 fn analyze_invalid_yaml() {
217 assert_invalid(
218 "name: memo\n\tpriority: high",
219 ContentType::Yaml,
220 "invalid-yaml",
221 );
222 }
223
224 #[test]
225 fn analyze_valid_xml() {
226 let result = analyze("<root><item>memo</item></root>");
227 assert_eq!(result.content_type, ContentType::Xml);
228 assert_eq!(result.validation.status, ValidationStatus::Valid);
229 }
230
231 #[test]
232 fn analyze_invalid_xml() {
233 assert_invalid("<root><item>memo</root>", ContentType::Xml, "invalid-xml");
234 }
235
236 #[test]
237 fn analyze_valid_markdown() {
238 let input = "# Inbox\n\n- [ ] buy milk";
239 let result = analyze(input);
240 assert_eq!(result.content_type, ContentType::Markdown);
241 assert_eq!(result.validation.status, ValidationStatus::Valid);
242 }
243
244 #[test]
245 fn analyze_invalid_markdown() {
246 let input = "```rust\nfn main() {}\n";
247 assert_invalid(input, ContentType::Markdown, "invalid-markdown");
248 }
249
250 #[test]
251 fn analyze_plain_text_fallback() {
252 let result = analyze("buy milk tomorrow");
253 assert_eq!(result.content_type, ContentType::Text);
254 assert_eq!(result.validation.status, ValidationStatus::Skipped);
255 assert!(result.validation.errors.is_none());
256 }
257
258 #[test]
259 fn analyze_empty_input_fallback() {
260 let result = analyze(" \n\t");
261 assert_eq!(result.content_type, ContentType::Unknown);
262 assert_eq!(result.validation.status, ValidationStatus::Unknown);
263 assert!(result.validation.errors.is_none());
264 }
265}