Skip to main content

lib_humus/templating/
format_chooser.rs

1// SPDX-FileCopyrightText: 2026 Slatian <baschdel@disroot.org>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5use std::collections::HashMap;
6
7use crate::headers::AcceptHeader;
8use crate::headers::AcceptHeaderItem;
9use crate::templating::HumusApiFormat;
10use crate::templating::HumusFormatIdentifier;
11use crate::templating::TemplatesManifest;
12
13/// Helper that given a [TemplatesManifest] can read a [HumusFormatIdentifier] from an HTTP `Accept` header.
14#[derive(Debug, Clone)]
15pub struct FormatChooser {
16	/// Data for media_type based choosing
17	accept_header_items: HashMap<AcceptHeaderItem, HumusFormatIdentifier>,
18
19	/// Data for user agent based chooing
20	user_agent_prefixes: Vec<(String, HumusFormatIdentifier)>,
21}
22
23impl FormatChooser {
24	/// Create a new format chooser from a parsed manifest and the `ApiFormat` type.
25	pub fn new<ApiFormat>(manifest: &TemplatesManifest) -> Self
26	where
27		ApiFormat: HumusApiFormat,
28	{
29		let mut accept_header_items: HashMap<AcceptHeaderItem, HumusFormatIdentifier> =
30			HashMap::new();
31
32		for (id, format_description) in &manifest.format {
33			// Skip format descriptions that would clash with api formats
34			if ApiFormat::from_name(id).is_some() {
35				continue;
36			}
37
38			// Register exact media type
39			accept_header_items.insert(
40				AcceptHeaderItem::MediaType(
41					format_description.media_type.essence_str().to_string(),
42				),
43				*id,
44			);
45			// Register wildcard `{type}/*`
46			accept_header_items.insert(
47				AcceptHeaderItem::PartialWildcard(
48					format_description.media_type.type_().to_string(),
49				),
50				*id,
51			);
52		}
53
54		for api_format in ApiFormat::get_all() {
55			let id = api_format.get_name();
56			let media_type = api_format.get_media_type();
57
58			// Register exact media type
59			accept_header_items.insert(
60				AcceptHeaderItem::MediaType(media_type.essence_str().to_string()),
61				id,
62			);
63			// Register wildcard `{type}/*`
64			accept_header_items.insert(
65				AcceptHeaderItem::PartialWildcard(media_type.type_().to_string()),
66				id,
67			);
68		}
69
70		// Register aliases so that they overwrite the defaults
71		for (item, id) in &manifest.media_type_aliases {
72			accept_header_items.insert(item.clone(), *id);
73		}
74
75		// Don't add a default value for the */* wildcard, it is not needed.
76
77		// Generate User agents
78
79		let mut user_agent_prefixes: Vec<(String, HumusFormatIdentifier)> = manifest
80			.format_by_user_agent_prefix
81			.iter()
82			.map(|(prefix, format)| (prefix.clone(), *format))
83			.collect();
84
85		// sort in descending order of length
86		user_agent_prefixes.sort_by_key(|(prefix_a, _)| std::cmp::Reverse(prefix_a.len()));
87
88		Self {
89			accept_header_items,
90			user_agent_prefixes,
91		}
92	}
93
94	/// Returns the best fitting [HumusFormatIdentifier] for the given header
95	pub fn choose_from_accept_header(
96		&self,
97		header: &AcceptHeader,
98	) -> Option<HumusFormatIdentifier> {
99		for (item, _) in &header.items {
100			if matches!(item, AcceptHeaderItem::Wildcard) {
101				return None;
102			}
103			if let Some(format) = self.accept_header_items.get(item) {
104				return Some(*format);
105			}
106		}
107		None
108	}
109
110	/// Returns the best fitting [HumusFormatIdentifier] for the given user agent
111	pub fn choose_from_user_agent(&self, user_agent: &str) -> Option<HumusFormatIdentifier> {
112		for (prefix, format) in &self.user_agent_prefixes {
113			if user_agent.starts_with(prefix) {
114				return Some(*format);
115			}
116		}
117		None
118	}
119}