systemprompt_api/services/middleware/negotiation/
mod.rs1use axum::extract::Request;
12use axum::middleware::Next;
13use axum::response::Response;
14
15#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
16pub enum AcceptedMediaType {
17 #[default]
18 Json,
19 Markdown,
20 Html,
21}
22
23impl AcceptedMediaType {
24 pub const fn content_type(&self) -> &'static str {
25 match self {
26 Self::Json => "application/json",
27 Self::Markdown => "text/markdown; charset=utf-8",
28 Self::Html => "text/html; charset=utf-8",
29 }
30 }
31
32 pub const fn is_markdown(&self) -> bool {
33 matches!(self, Self::Markdown)
34 }
35}
36
37#[derive(Debug, Clone, Copy)]
38pub struct AcceptedFormat(pub AcceptedMediaType);
39
40impl Default for AcceptedFormat {
41 fn default() -> Self {
42 Self(AcceptedMediaType::Json)
43 }
44}
45
46impl AcceptedFormat {
47 pub const fn media_type(&self) -> AcceptedMediaType {
48 self.0
49 }
50
51 pub const fn is_markdown(&self) -> bool {
52 self.0.is_markdown()
53 }
54}
55
56struct MediaTypeEntry {
57 media_type: AcceptedMediaType,
58 quality: f32,
59}
60
61pub fn parse_accept_header(header_value: &str) -> AcceptedFormat {
62 let mut entries = Vec::new();
63
64 for part in header_value.split(',') {
65 let part = part.trim();
66 if part.is_empty() {
67 continue;
68 }
69
70 let (media_type_str, params) = part
71 .split_once(';')
72 .map_or((part, ""), |(m, p)| (m.trim(), p));
73
74 let quality = params
75 .split(';')
76 .find_map(|p| {
77 let p = p.trim();
78 p.strip_prefix("q=")
79 .and_then(|q_str| q_str.parse::<f32>().ok().map(|q| q.clamp(0.0, 1.0)))
80 })
81 .unwrap_or(1.0);
82
83 let media_type = match media_type_str.to_lowercase().as_str() {
84 "text/markdown" | "text/x-markdown" => Some(AcceptedMediaType::Markdown),
85 "application/json" | "*/*" => Some(AcceptedMediaType::Json),
86 "text/html" | "application/xhtml+xml" => Some(AcceptedMediaType::Html),
87 _ => None,
88 };
89
90 if let Some(mt) = media_type {
91 entries.push(MediaTypeEntry {
92 media_type: mt,
93 quality,
94 });
95 }
96 }
97
98 entries.sort_by(|a, b| {
99 b.quality
100 .partial_cmp(&a.quality)
101 .unwrap_or(std::cmp::Ordering::Equal)
102 });
103
104 let media_type = entries
105 .first()
106 .map_or(AcceptedMediaType::Json, |e| e.media_type);
107
108 AcceptedFormat(media_type)
109}
110
111pub async fn content_negotiation_middleware(mut request: Request, next: Next) -> Response {
112 let accepted_format = request
113 .headers()
114 .get(http::header::ACCEPT)
115 .and_then(|v| v.to_str().ok())
116 .map_or_else(AcceptedFormat::default, parse_accept_header);
117
118 request.extensions_mut().insert(accepted_format);
119
120 next.run(request).await
121}