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 axum::{body::Body, http::Request};
use std::sync::Arc;
use tower::{Layer, Service};

use crate::HumusQuerySettings;
use crate::language::LanguageManifest;
use crate::language::UnicodeLanguageIdentifier;

use crate::middleware::format_extraction::extract_format_from_request_accept_header;
use crate::middleware::format_extraction::extract_format_from_request_uri;
use crate::middleware::format_extraction::extract_format_from_request_user_agent_header;
use crate::middleware::language_extraction::language_from_accept_language_header;
use crate::middleware::language_extraction::language_from_cookie;
use crate::templating::FormatChooser;
use crate::templating::HumusFormatIdentifier;
use crate::templating::TemplatesManifest;

/// A tower and axum compatible layer that extracts language information from the request and provides it as a [TemplateSetting] extension.
///
/// If you alread have a [HumusEngine][crate::HumusEngine] take a look at its [get_template_settings_layer][crate::HumusEngine::get_template_settings_layer] method.
///
/// For details see the [TemplateSettingsService].
#[derive(Debug, Clone)]
pub struct TemplateSettingsLayer {
	templates_manifest: Arc<TemplatesManifest>,
	language_manifest: Arc<LanguageManifest>,
	format_chooser: Arc<FormatChooser>,
}

impl TemplateSettingsLayer {
	/// Creates a new TemplateSettingsLayer from a language manifest
	pub fn new(
		templates_manifest: TemplatesManifest,
		language_manifest: LanguageManifest,
		format_chooser: FormatChooser,
	) -> Self {
		Self {
			templates_manifest: Arc::new(templates_manifest),
			language_manifest: Arc::new(language_manifest),
			format_chooser: Arc::new(format_chooser),
		}
	}
}

impl<S> Layer<S> for TemplateSettingsLayer {
	type Service = TemplateSettingsService<S>;

	fn layer(&self, inner: S) -> Self::Service {
		TemplateSettingsService {
			templates_manifest: self.templates_manifest.clone(),
			language_manifest: self.language_manifest.clone(),
			format_chooser: self.format_chooser.clone(),
			service: inner,
		}
	}
}

/// Service created by the [TemplateSettingsLayer] (you probably want to construct the layer)
///
/// It extracts language information and provides the [TemplateSetting] extension for every request that passes through it.
///
/// Language information is sourced from two places:
/// * The `lib_humus_language` cookie which may contain [UnicodeLanguageIdentifier]
/// * The `Accept-Language` header where it uses the first language that is also present in the language manifest.
///
/// Format information is sources from:
/// * The `format` parameter, this is not validated here as validation is left to the templating engine
/// * The `Accept` header using information from the templates manifest
#[derive(Debug, Clone)]
pub struct TemplateSettingsService<S> {
	templates_manifest: Arc<TemplatesManifest>,
	format_chooser: Arc<FormatChooser>,
	language_manifest: Arc<LanguageManifest>,
	service: S,
}

impl<S> Service<Request<Body>> for TemplateSettingsService<S>
where
	S: Service<Request<Body>>,
{
	type Response = S::Response;
	type Error = S::Error;
	type Future = S::Future;

	fn poll_ready(
		&mut self,
		cx: &mut std::task::Context<'_>,
	) -> std::task::Poll<Result<(), Self::Error>> {
		self.service.poll_ready(cx)
	}

	fn call(&mut self, mut req: Request<Body>) -> Self::Future {
		let mut best_language: Option<UnicodeLanguageIdentifier> = None;

		if let Some(language) = language_from_cookie(&req, &self.language_manifest) {
			best_language = Some(language);
		} else if let Some(language) =
			language_from_accept_language_header(&req, &self.language_manifest)
		{
			best_language = Some(language);
		}

		let format_id = if let Some(id) = extract_format_from_request_uri(&req) {
			id
		} else if let Some(id) =
			extract_format_from_request_accept_header(&req, &self.format_chooser)
		{
			id
		} else if let Some(id) =
			extract_format_from_request_user_agent_header(&req, &self.format_chooser)
		{
			id
		} else {
			self.templates_manifest.default_format
		};

		// Put TemplateLanguageSetting on request
		req.extensions_mut().insert(TemplateSetting {
			language: best_language,
			format: format_id,
		});

		self.service.call(req)
	}
}

/// Language and format setting for the template
///
/// It is added as an extension to the request by the [TemplateSettingsLayer].
///
/// When using axum you can retrieve it using:
///
/// ```rust,ignore
/// async fn some_handler(
/// 	// … other arguments …,
/// 	Extension(settings): Extension<TemplateSetting>,
/// ) -> Response {
/// 	// TODO
/// }
/// ```
#[derive(Debug, Clone)]
pub struct TemplateSetting {
	/// The language to use for the template, None means that no preference could be
	/// extracted and that the backend should use the default languge.
	pub language: Option<UnicodeLanguageIdentifier>,

	/// The format identifier for giving to the templating engine.
	pub format: HumusFormatIdentifier,
}

impl HumusQuerySettings for TemplateSetting {
	fn get_format(&self) -> HumusFormatIdentifier {
		self.format
	}

	fn get_preferred_language(&self) -> Option<UnicodeLanguageIdentifier> {
		self.language.clone()
	}
}