lib-humus 0.6.0

Helps creating configurable frontends for humans and computers using axum, Tera and toml.
Documentation
// SPDX-FileCopyrightText: 2026 Slatian <baschdel@disroot.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

use std::collections::HashMap;

use crate::headers::AcceptHeader;
use crate::headers::AcceptHeaderItem;
use crate::templating::HumusApiFormat;
use crate::templating::HumusFormatIdentifier;
use crate::templating::TemplatesManifest;

/// Helper that given a [TemplatesManifest] can read a [HumusFormatIdentifier] from an HTTP `Accept` header.
#[derive(Debug, Clone)]
pub struct FormatChooser {
	/// Data for media_type based choosing
	accept_header_items: HashMap<AcceptHeaderItem, HumusFormatIdentifier>,

	/// Data for user agent based chooing
	user_agent_prefixes: Vec<(String, HumusFormatIdentifier)>,
}

impl FormatChooser {
	/// Create a new format chooser from a parsed manifest and the `ApiFormat` type.
	pub fn new<ApiFormat>(manifest: &TemplatesManifest) -> Self
	where
		ApiFormat: HumusApiFormat,
	{
		let mut accept_header_items: HashMap<AcceptHeaderItem, HumusFormatIdentifier> =
			HashMap::new();

		for (id, format_description) in &manifest.format {
			// Skip format descriptions that would clash with api formats
			if ApiFormat::from_name(id).is_some() {
				continue;
			}

			// Register exact media type
			accept_header_items.insert(
				AcceptHeaderItem::MediaType(
					format_description.media_type.essence_str().to_string(),
				),
				*id,
			);
			// Register wildcard `{type}/*`
			accept_header_items.insert(
				AcceptHeaderItem::PartialWildcard(
					format_description.media_type.type_().to_string(),
				),
				*id,
			);
		}

		for api_format in ApiFormat::get_all() {
			let id = api_format.get_name();
			let media_type = api_format.get_media_type();

			// Register exact media type
			accept_header_items.insert(
				AcceptHeaderItem::MediaType(media_type.essence_str().to_string()),
				id,
			);
			// Register wildcard `{type}/*`
			accept_header_items.insert(
				AcceptHeaderItem::PartialWildcard(media_type.type_().to_string()),
				id,
			);
		}

		// Register aliases so that they overwrite the defaults
		for (item, id) in &manifest.media_type_aliases {
			accept_header_items.insert(item.clone(), *id);
		}

		// Don't add a default value for the */* wildcard, it is not needed.

		// Generate User agents

		let mut user_agent_prefixes: Vec<(String, HumusFormatIdentifier)> = manifest
			.format_by_user_agent_prefix
			.iter()
			.map(|(prefix, format)| (prefix.clone(), *format))
			.collect();

		// sort in descending order of length
		user_agent_prefixes.sort_by_key(|(prefix_a, _)| std::cmp::Reverse(prefix_a.len()));

		Self {
			accept_header_items,
			user_agent_prefixes,
		}
	}

	/// Returns the best fitting [HumusFormatIdentifier] for the given header
	pub fn choose_from_accept_header(
		&self,
		header: &AcceptHeader,
	) -> Option<HumusFormatIdentifier> {
		for (item, _) in &header.items {
			if matches!(item, AcceptHeaderItem::Wildcard) {
				return None;
			}
			if let Some(format) = self.accept_header_items.get(item) {
				return Some(*format);
			}
		}
		None
	}

	/// Returns the best fitting [HumusFormatIdentifier] for the given user agent
	pub fn choose_from_user_agent(&self, user_agent: &str) -> Option<HumusFormatIdentifier> {
		for (prefix, format) in &self.user_agent_prefixes {
			if user_agent.starts_with(prefix) {
				return Some(*format);
			}
		}
		None
	}
}