1use crate::{TOON_CONTENT_TYPE, TOON_CONTENT_TYPE_TEXT};
7use http::{header, StatusCode};
8use rustapi_core::{ApiError, FromRequestParts, IntoResponse, Request, Response};
9use rustapi_openapi::{
10 MediaType, Operation, OperationModifier, ResponseModifier, ResponseSpec, SchemaRef,
11};
12use serde::Serialize;
13use std::collections::BTreeMap;
14
15pub const JSON_CONTENT_TYPE: &str = "application/json";
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum OutputFormat {
21 #[default]
23 Json,
24 Toon,
26}
27
28impl OutputFormat {
29 pub fn content_type(&self) -> &'static str {
31 match self {
32 OutputFormat::Json => JSON_CONTENT_TYPE,
33 OutputFormat::Toon => TOON_CONTENT_TYPE,
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
46pub struct AcceptHeader {
47 pub preferred: OutputFormat,
49 pub media_types: Vec<MediaTypeEntry>,
51}
52
53#[derive(Debug, Clone)]
55pub struct MediaTypeEntry {
56 pub media_type: String,
58 pub quality: f32,
60}
61
62impl Default for AcceptHeader {
63 fn default() -> Self {
64 Self {
65 preferred: OutputFormat::Json,
66 media_types: vec![MediaTypeEntry {
67 media_type: JSON_CONTENT_TYPE.to_string(),
68 quality: 1.0,
69 }],
70 }
71 }
72}
73
74impl AcceptHeader {
75 pub fn parse(header_value: &str) -> Self {
77 let mut entries: Vec<MediaTypeEntry> = header_value
78 .split(',')
79 .filter_map(|part| {
80 let part = part.trim();
81 if part.is_empty() {
82 return None;
83 }
84
85 let (media_type, quality) = if let Some(q_pos) = part.find(";q=") {
86 let (mt, q_part) = part.split_at(q_pos);
87 let q_str = q_part.trim_start_matches(";q=").trim();
88 let quality = q_str.parse::<f32>().unwrap_or(1.0).clamp(0.0, 1.0);
89 (mt.trim().to_string(), quality)
90 } else if let Some(semi_pos) = part.find(';') {
91 (part[..semi_pos].trim().to_string(), 1.0)
93 } else {
94 (part.to_string(), 1.0)
95 };
96
97 Some(MediaTypeEntry {
98 media_type,
99 quality,
100 })
101 })
102 .collect();
103
104 entries.sort_by(|a, b| {
106 b.quality
107 .partial_cmp(&a.quality)
108 .unwrap_or(std::cmp::Ordering::Equal)
109 });
110
111 let preferred = Self::determine_format(&entries);
113
114 Self {
115 preferred,
116 media_types: entries,
117 }
118 }
119
120 fn determine_format(entries: &[MediaTypeEntry]) -> OutputFormat {
122 for entry in entries {
123 let mt = entry.media_type.to_lowercase();
124
125 if mt == TOON_CONTENT_TYPE || mt == TOON_CONTENT_TYPE_TEXT {
127 return OutputFormat::Toon;
128 }
129
130 if mt == JSON_CONTENT_TYPE || mt == "application/json" || mt == "text/json" {
132 return OutputFormat::Json;
133 }
134
135 if mt == "*/*" || mt == "application/*" || mt == "text/*" {
137 return OutputFormat::Json;
138 }
139 }
140
141 OutputFormat::Json
143 }
144
145 pub fn accepts_toon(&self) -> bool {
147 self.media_types.iter().any(|e| {
148 let mt = e.media_type.to_lowercase();
149 mt == TOON_CONTENT_TYPE
150 || mt == TOON_CONTENT_TYPE_TEXT
151 || mt == "*/*"
152 || mt == "application/*"
153 })
154 }
155
156 pub fn accepts_json(&self) -> bool {
158 self.media_types.iter().any(|e| {
159 let mt = e.media_type.to_lowercase();
160 mt == JSON_CONTENT_TYPE || mt == "text/json" || mt == "*/*" || mt == "application/*"
161 })
162 }
163}
164
165impl FromRequestParts for AcceptHeader {
166 fn from_request_parts(req: &Request) -> rustapi_core::Result<Self> {
167 let accept = req
168 .headers()
169 .get(header::ACCEPT)
170 .and_then(|v| v.to_str().ok())
171 .map(AcceptHeader::parse)
172 .unwrap_or_default();
173
174 Ok(accept)
175 }
176}
177
178#[derive(Debug, Clone)]
210pub struct Negotiate<T> {
211 pub data: T,
213 pub format: OutputFormat,
215}
216
217impl<T> Negotiate<T> {
218 pub fn new(data: T, format: OutputFormat) -> Self {
220 Self { data, format }
221 }
222
223 pub fn json(data: T) -> Self {
225 Self {
226 data,
227 format: OutputFormat::Json,
228 }
229 }
230
231 pub fn toon(data: T) -> Self {
233 Self {
234 data,
235 format: OutputFormat::Toon,
236 }
237 }
238
239 pub fn format(&self) -> OutputFormat {
241 self.format
242 }
243}
244
245impl<T: Serialize> IntoResponse for Negotiate<T> {
246 fn into_response(self) -> Response {
247 match self.format {
248 OutputFormat::Json => match serde_json::to_vec(&self.data) {
249 Ok(body) => http::Response::builder()
250 .status(StatusCode::OK)
251 .header(header::CONTENT_TYPE, JSON_CONTENT_TYPE)
252 .body(rustapi_core::ResponseBody::from(body))
253 .unwrap(),
254 Err(err) => {
255 let error = ApiError::internal(format!("JSON serialization error: {}", err));
256 error.into_response()
257 }
258 },
259 OutputFormat::Toon => match toon_format::encode_default(&self.data) {
260 Ok(body) => http::Response::builder()
261 .status(StatusCode::OK)
262 .header(header::CONTENT_TYPE, TOON_CONTENT_TYPE)
263 .body(rustapi_core::ResponseBody::from(body))
264 .unwrap(),
265 Err(err) => {
266 let error = ApiError::internal(format!("TOON serialization error: {}", err));
267 error.into_response()
268 }
269 },
270 }
271 }
272}
273
274impl<T: Send> OperationModifier for Negotiate<T> {
276 fn update_operation(_op: &mut Operation) {
277 }
279}
280
281impl<T: Serialize> ResponseModifier for Negotiate<T> {
282 fn update_response(op: &mut Operation) {
283 let mut content = BTreeMap::new();
284
285 content.insert(
287 JSON_CONTENT_TYPE.to_string(),
288 MediaType {
289 schema: Some(SchemaRef::Inline(serde_json::json!({
290 "type": "object",
291 "description": "JSON formatted response"
292 }))),
293 example: None,
294 },
295 );
296
297 content.insert(
299 TOON_CONTENT_TYPE.to_string(),
300 MediaType {
301 schema: Some(SchemaRef::Inline(serde_json::json!({
302 "type": "string",
303 "description": "TOON (Token-Oriented Object Notation) formatted response"
304 }))),
305 example: None,
306 },
307 );
308
309 let response = ResponseSpec {
310 description: "Content-negotiated response (JSON or TOON based on Accept header)"
311 .to_string(),
312 content,
313 headers: BTreeMap::new(),
314 };
315 op.responses.insert("200".to_string(), response);
316 }
317}
318
319impl OperationModifier for AcceptHeader {
321 fn update_operation(_op: &mut Operation) {
322 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn test_accept_header_parse_json() {
332 let accept = AcceptHeader::parse("application/json");
333 assert_eq!(accept.preferred, OutputFormat::Json);
334 assert!(accept.accepts_json());
335 }
336
337 #[test]
338 fn test_accept_header_parse_toon() {
339 let accept = AcceptHeader::parse("application/toon");
340 assert_eq!(accept.preferred, OutputFormat::Toon);
341 assert!(accept.accepts_toon());
342 }
343
344 #[test]
345 fn test_accept_header_parse_with_quality() {
346 let accept = AcceptHeader::parse("application/json;q=0.5, application/toon;q=0.9");
347 assert_eq!(accept.preferred, OutputFormat::Toon);
348 assert_eq!(accept.media_types.len(), 2);
349 assert_eq!(accept.media_types[0].media_type, "application/toon");
351 assert_eq!(accept.media_types[0].quality, 0.9);
352 }
353
354 #[test]
355 fn test_accept_header_parse_wildcard() {
356 let accept = AcceptHeader::parse("*/*");
357 assert_eq!(accept.preferred, OutputFormat::Json);
358 assert!(accept.accepts_json());
359 assert!(accept.accepts_toon());
360 }
361
362 #[test]
363 fn test_accept_header_parse_multiple() {
364 let accept = AcceptHeader::parse("text/html, application/json, application/toon;q=0.8");
365 assert_eq!(accept.preferred, OutputFormat::Json);
367 }
368
369 #[test]
370 fn test_accept_header_default() {
371 let accept = AcceptHeader::default();
372 assert_eq!(accept.preferred, OutputFormat::Json);
373 }
374
375 #[test]
376 fn test_output_format_content_type() {
377 assert_eq!(OutputFormat::Json.content_type(), "application/json");
378 assert_eq!(OutputFormat::Toon.content_type(), "application/toon");
379 }
380}