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