rustapi_toon/
negotiate.rs1use 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::HashMap;
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 = HashMap::new();
284
285 content.insert(
287 JSON_CONTENT_TYPE.to_string(),
288 MediaType {
289 schema: SchemaRef::Inline(serde_json::json!({
290 "type": "object",
291 "description": "JSON formatted response"
292 })),
293 },
294 );
295
296 content.insert(
298 TOON_CONTENT_TYPE.to_string(),
299 MediaType {
300 schema: SchemaRef::Inline(serde_json::json!({
301 "type": "string",
302 "description": "TOON (Token-Oriented Object Notation) formatted response"
303 })),
304 },
305 );
306
307 let response = ResponseSpec {
308 description: "Content-negotiated response (JSON or TOON based on Accept header)"
309 .to_string(),
310 content: Some(content),
311 };
312 op.responses.insert("200".to_string(), response);
313 }
314}
315
316impl OperationModifier for AcceptHeader {
318 fn update_operation(_op: &mut Operation) {
319 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn test_accept_header_parse_json() {
329 let accept = AcceptHeader::parse("application/json");
330 assert_eq!(accept.preferred, OutputFormat::Json);
331 assert!(accept.accepts_json());
332 }
333
334 #[test]
335 fn test_accept_header_parse_toon() {
336 let accept = AcceptHeader::parse("application/toon");
337 assert_eq!(accept.preferred, OutputFormat::Toon);
338 assert!(accept.accepts_toon());
339 }
340
341 #[test]
342 fn test_accept_header_parse_with_quality() {
343 let accept = AcceptHeader::parse("application/json;q=0.5, application/toon;q=0.9");
344 assert_eq!(accept.preferred, OutputFormat::Toon);
345 assert_eq!(accept.media_types.len(), 2);
346 assert_eq!(accept.media_types[0].media_type, "application/toon");
348 assert_eq!(accept.media_types[0].quality, 0.9);
349 }
350
351 #[test]
352 fn test_accept_header_parse_wildcard() {
353 let accept = AcceptHeader::parse("*/*");
354 assert_eq!(accept.preferred, OutputFormat::Json);
355 assert!(accept.accepts_json());
356 assert!(accept.accepts_toon());
357 }
358
359 #[test]
360 fn test_accept_header_parse_multiple() {
361 let accept = AcceptHeader::parse("text/html, application/json, application/toon;q=0.8");
362 assert_eq!(accept.preferred, OutputFormat::Json);
364 }
365
366 #[test]
367 fn test_accept_header_default() {
368 let accept = AcceptHeader::default();
369 assert_eq!(accept.preferred, OutputFormat::Json);
370 }
371
372 #[test]
373 fn test_output_format_content_type() {
374 assert_eq!(OutputFormat::Json.content_type(), "application/json");
375 assert_eq!(OutputFormat::Toon.content_type(), "application/toon");
376 }
377}