Skip to main content

lib_humus/middleware/
template_settings.rs

1// SPDX-FileCopyrightText: 2026 Slatian <baschdel@disroot.org>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5use axum::{body::Body, http::Request};
6use std::sync::Arc;
7use tower::{Layer, Service};
8
9use crate::HumusQuerySettings;
10use crate::language::LanguageManifest;
11use crate::language::UnicodeLanguageIdentifier;
12
13use crate::middleware::format_extraction::extract_format_from_request_accept_header;
14use crate::middleware::format_extraction::extract_format_from_request_uri;
15use crate::middleware::format_extraction::extract_format_from_request_user_agent_header;
16use crate::middleware::language_extraction::language_from_accept_language_header;
17use crate::middleware::language_extraction::language_from_cookie;
18use crate::templating::FormatChooser;
19use crate::templating::HumusFormatIdentifier;
20use crate::templating::TemplatesManifest;
21
22/// A tower and axum compatible layer that extracts language information from the request and provides it as a [TemplateSetting] extension.
23///
24/// If you alread have a [HumusEngine][crate::HumusEngine] take a look at its [get_template_settings_layer][crate::HumusEngine::get_template_settings_layer] method.
25///
26/// For details see the [TemplateSettingsService].
27#[derive(Debug, Clone)]
28pub struct TemplateSettingsLayer {
29	templates_manifest: Arc<TemplatesManifest>,
30	language_manifest: Arc<LanguageManifest>,
31	format_chooser: Arc<FormatChooser>,
32}
33
34impl TemplateSettingsLayer {
35	/// Creates a new TemplateSettingsLayer from a language manifest
36	pub fn new(
37		templates_manifest: TemplatesManifest,
38		language_manifest: LanguageManifest,
39		format_chooser: FormatChooser,
40	) -> Self {
41		Self {
42			templates_manifest: Arc::new(templates_manifest),
43			language_manifest: Arc::new(language_manifest),
44			format_chooser: Arc::new(format_chooser),
45		}
46	}
47}
48
49impl<S> Layer<S> for TemplateSettingsLayer {
50	type Service = TemplateSettingsService<S>;
51
52	fn layer(&self, inner: S) -> Self::Service {
53		TemplateSettingsService {
54			templates_manifest: self.templates_manifest.clone(),
55			language_manifest: self.language_manifest.clone(),
56			format_chooser: self.format_chooser.clone(),
57			service: inner,
58		}
59	}
60}
61
62/// Service created by the [TemplateSettingsLayer] (you probably want to construct the layer)
63///
64/// It extracts language information and provides the [TemplateSetting] extension for every request that passes through it.
65///
66/// Language information is sourced from two places:
67/// * The `lib_humus_language` cookie which may contain [UnicodeLanguageIdentifier]
68/// * The `Accept-Language` header where it uses the first language that is also present in the language manifest.
69///
70/// Format information is sources from:
71/// * The `format` parameter, this is not validated here as validation is left to the templating engine
72/// * The `Accept` header using information from the templates manifest
73#[derive(Debug, Clone)]
74pub struct TemplateSettingsService<S> {
75	templates_manifest: Arc<TemplatesManifest>,
76	format_chooser: Arc<FormatChooser>,
77	language_manifest: Arc<LanguageManifest>,
78	service: S,
79}
80
81impl<S> Service<Request<Body>> for TemplateSettingsService<S>
82where
83	S: Service<Request<Body>>,
84{
85	type Response = S::Response;
86	type Error = S::Error;
87	type Future = S::Future;
88
89	fn poll_ready(
90		&mut self,
91		cx: &mut std::task::Context<'_>,
92	) -> std::task::Poll<Result<(), Self::Error>> {
93		self.service.poll_ready(cx)
94	}
95
96	fn call(&mut self, mut req: Request<Body>) -> Self::Future {
97		let mut best_language: Option<UnicodeLanguageIdentifier> = None;
98
99		if let Some(language) = language_from_cookie(&req, &self.language_manifest) {
100			best_language = Some(language);
101		} else if let Some(language) =
102			language_from_accept_language_header(&req, &self.language_manifest)
103		{
104			best_language = Some(language);
105		}
106
107		let format_id = if let Some(id) = extract_format_from_request_uri(&req) {
108			id
109		} else if let Some(id) =
110			extract_format_from_request_accept_header(&req, &self.format_chooser)
111		{
112			id
113		} else if let Some(id) =
114			extract_format_from_request_user_agent_header(&req, &self.format_chooser)
115		{
116			id
117		} else {
118			self.templates_manifest.default_format
119		};
120
121		// Put TemplateLanguageSetting on request
122		req.extensions_mut().insert(TemplateSetting {
123			language: best_language,
124			format: format_id,
125		});
126
127		self.service.call(req)
128	}
129}
130
131/// Language and format setting for the template
132///
133/// It is added as an extension to the request by the [TemplateSettingsLayer].
134///
135/// When using axum you can retrieve it using:
136///
137/// ```rust,ignore
138/// async fn some_handler(
139/// 	// … other arguments …,
140/// 	Extension(settings): Extension<TemplateSetting>,
141/// ) -> Response {
142/// 	// TODO
143/// }
144/// ```
145#[derive(Debug, Clone)]
146pub struct TemplateSetting {
147	/// The language to use for the template, None means that no preference could be
148	/// extracted and that the backend should use the default languge.
149	pub language: Option<UnicodeLanguageIdentifier>,
150
151	/// The format identifier for giving to the templating engine.
152	pub format: HumusFormatIdentifier,
153}
154
155impl HumusQuerySettings for TemplateSetting {
156	fn get_format(&self) -> HumusFormatIdentifier {
157		self.format
158	}
159
160	fn get_preferred_language(&self) -> Option<UnicodeLanguageIdentifier> {
161		self.language.clone()
162	}
163}