Skip to main content

lib_humus/headers/
accept_header.rs

1// SPDX-FileCopyrightText: 2026 Slatian <baschdel@disroot.org>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5use std::{fmt::Display, str::FromStr};
6
7use axum::extract::OptionalFromRequestParts;
8
9use crate::headers::parser::HeaderParser;
10
11/// Part of the [AcceptHeader]
12///
13/// TODO: implement precedence and media type parameters (<https://httpwg.org/specs/rfc9110.html#field.accept>)
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub enum AcceptHeaderItem {
16	/// Parsed from a `*/*`
17	Wildcard,
18
19	/// Parsed from a `{family}/*` contains the value of the family
20	PartialWildcard(String),
21
22	/// Parsed from a full media type
23	MediaType(String),
24}
25
26impl Display for AcceptHeaderItem {
27	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28		match self {
29			Self::Wildcard => write!(f, "*/*"),
30			Self::PartialWildcard(media_type) => write!(f, "{media_type}/*"),
31			Self::MediaType(media_type) => write!(f, "{media_type}"),
32		}
33	}
34}
35
36impl FromStr for AcceptHeaderItem {
37	type Err = String;
38
39	fn from_str(s: &str) -> Result<Self, Self::Err> {
40		let mut parser = HeaderParser::new(s);
41		if let Some(item) = parser.parse_accept_header_item() {
42			if !parser.is_at_end() {
43				return Err(format!(
44					"Found additional data after media type or wildcard. Expexted text to end after '{item}'"
45				));
46			}
47			return Ok(item);
48		} else {
49			return Err("Unable to parse accept header item, expected some/type[+foo], some/* or */* as imput.".to_string());
50		}
51	}
52}
53
54impl<'de> serde::de::Deserialize<'de> for AcceptHeaderItem {
55	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
56	where
57		D: serde::Deserializer<'de>,
58	{
59		deserializer.deserialize_str(AcceptHeaderItemVisitor)
60	}
61}
62
63struct AcceptHeaderItemVisitor;
64
65impl<'de> serde::de::Visitor<'de> for AcceptHeaderItemVisitor {
66	type Value = AcceptHeaderItem;
67
68	fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
69		write!(
70			f,
71			"The media_type part of an HTTP Accept header, i.e. some/type[+foo], some/* or */*"
72		)
73	}
74
75	fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
76	where
77		E: serde::de::Error,
78	{
79		AcceptHeaderItem::from_str(v).map_err(|e| E::custom(e.to_string()))
80	}
81}
82
83impl serde::Serialize for AcceptHeaderItem {
84	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
85	where
86		S: serde::Serializer,
87	{
88		serializer.serialize_str(self.to_string().as_str())
89	}
90}
91
92/// A parsed [`Accept` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept) that can also be used as an optional axum extractor
93#[derive(Debug, Clone, PartialEq)]
94pub struct AcceptHeader {
95	/// Items in descending order of preference,
96	pub items: Vec<(AcceptHeaderItem, f32)>,
97}
98
99impl AcceptHeader {
100	/// Creates a new Parsed header from header text. The parser will stop when it encounterrs syntax it doesn't understand.
101	pub fn new(header_text: &str) -> Self {
102		HeaderParser::new(header_text).parse_accept()
103	}
104}
105
106impl FromStr for AcceptHeader {
107	type Err = ();
108
109	fn from_str(s: &str) -> Result<Self, Self::Err> {
110		Ok(HeaderParser::new(s).parse_accept())
111	}
112}
113
114impl<S> OptionalFromRequestParts<S> for AcceptHeader
115where
116	S: Sync,
117{
118	type Rejection = ();
119
120	async fn from_request_parts(
121		parts: &mut axum::http::request::Parts,
122		_state: &S,
123	) -> Result<Option<Self>, Self::Rejection> {
124		Ok(parts
125			.headers
126			.get("accept")
127			.and_then(|v| Some(AcceptHeader::new(v.to_str().ok()?))))
128	}
129}