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, HashSet};

use serde::{Deserialize, Serialize};

use crate::language::{UnicodeLanguageIdentifier, variable_description::VariableDescription};

use crate::headers::AcceptLanguageHeader;
use crate::headers::AcceptLanguageHeaderLanguage;
use crate::templating::HumusFormatIdentifier;

/// Description of available languages for a [LanguageEngine][crate::language::LanguageEngine].
///
/// It is typically read from the `languages/manifest.toml` in the templates directory.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LanguageManifest {
	/// A map of available languages
	#[serde(rename = "language")]
	pub languages: HashMap<UnicodeLanguageIdentifier, LanguageDescriptor>,
	/// The default fallback language that **must** also a valid entry in `languages`.
	pub default_language: UnicodeLanguageIdentifier,
	/// Lists all ids that are expected to be in the translation files
	pub available_messages: HashSet<String>,
	/// Decribes messages that are more than just an id to language mapping further.
	#[serde(rename = "message", default)]
	pub message_descriptions: HashMap<String, MessageDescription>,
	/// Describes how the language engine should behave regarding different output formats.
	#[serde(rename = "format", default)]
	pub formats: HashMap<HumusFormatIdentifier, FormatDescriptor>,
}

impl LanguageManifest {
	/// Creates a new language manifest with an undefined language.
	pub fn new_empty() -> Self {
		Self {
			default_language: unic_langid::langid!("und").into(),
			languages: [(
				unic_langid::langid!("und").into(),
				LanguageDescriptor {
					name: "Undefined".to_string(),
					abbreviation: "UND".to_string(),
					is_hidden: false,
				},
			)]
			.into_iter()
			.collect(),
			available_messages: Default::default(),
			message_descriptions: Default::default(),
			formats: Default::default(),
		}
	}

	/// Returns wheather the given language is defined as non-hidden in the manifest
	pub fn has_visible_language(&self, language: &UnicodeLanguageIdentifier) -> bool {
		self.languages
			.get(language)
			.map(|desc| !desc.is_hidden)
			.unwrap_or(false)
	}

	/// Returns the best fitting language that is in the manifest for a given `Accept-Language` header.
	///
	/// It returns `None` if there is no explicit match.
	pub fn get_best_fitting_language(
		&self,
		header: &AcceptLanguageHeader,
	) -> Option<UnicodeLanguageIdentifier> {
		let mut best_fitting: Option<UnicodeLanguageIdentifier> = None;
		for (l, _) in &header.languages {
			match l {
				AcceptLanguageHeaderLanguage::Wildcard => {
					// At this point we should have found a matching language, everything after is lower prefeerence
					break;
				}
				AcceptLanguageHeaderLanguage::Code(code) => {
					if let Ok(mut lang) = code.parse::<UnicodeLanguageIdentifier>() {
						if self.has_visible_language(&lang) {
							return Some(lang);
						}
						lang.script = None;
						lang.region = None;
						if self.has_visible_language(&lang) {
							best_fitting = Some(lang);
						}
					}
				}
			}
		}
		best_fitting
	}

	/// Returns the safe suffix for the given formaqt if configured
	pub fn get_safe_suffix_for_format(&self, format: HumusFormatIdentifier) -> Option<String> {
		self.formats.get(&format)?.safe_suffix.clone()
	}
}

/// Part of the [LanguageManifest], describes a single language.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LanguageDescriptor {
	/// Human readable name of the language to show in language selectors
	///
	/// Best practice is to write the language down with it's own name.
	///
	/// Example: `English`, `Deutsch`
	pub name: String,

	/// Human readable abbreviation to show in language selectors
	pub abbreviation: String,

	/// Weather the entry should be hidden in language selectors, defaults to `false`.
	///
	/// Hidden languages are not consideredd when evaluating the `Acept-Language` header.
	///
	/// This is useful for testing locales.
	#[serde(default)]
	pub is_hidden: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MessageDescription {
	#[serde(default, rename = "arg")]
	pub arguments: HashMap<String, VariableDescription>,
}

/// Describes an output format for the template
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FormatDescriptor {
	/// When a message_id has this suffix its output will automatically be arked as safe and all string arguments will be encoded unless they also have the `safe_suffix`.
	#[serde(default)]
	pub safe_suffix: Option<String>,
}