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 log::error;
use log::warn;
use tera::Tera;

use std::marker::PhantomData;
use std::path::PathBuf;
use std::sync::Arc;

use crate::HumusEngine;
use crate::language::LanguageEngine;
use crate::language::LanguageEngineLoaderError;
use crate::templating::HumusApiFormat;
use crate::templating::TemplatingEngine;
use crate::templating::TemplatingEngineLoaderError;

/// Helps loading a [HumusEngine] from disk.
///
/// This merges settings from a configuration file and from command line arguments and then loads both the [TemplatingEngine] and [LanguageEngine] into a working [HumusEngine].
///
/// For documentation of the on disk format see [the template directory documentation](crate::doc::template_directory).
///
/// The functions doing the actual work are [TemplatingEngine::load_from_directory] and  [LanguageEngine::load_from_directory].
///
/// Example:
/// ```rust,ignore
/// use lib_humus::HumusEngineLoader;
/// use lib_humus::crates::tera::Tera;
///
/// let template_loader = HumusEngineLoader::new(
/// 	config.template.template_location.clone(),
/// 	config.template.extra_config.clone(),
/// 	Tera::new(),
/// )
/// 	.cli_template_location(cli_args.template_location)
/// 	.cli_extra_config_location(cli_args.extra_config);
///
///
/// let templating_engine = match template_loader.load_templates() {
/// 	Ok(t) => t.into(),
/// 	Err(e) => {
/// 		println!("{e}");
/// 		::std::process::exit(1);
/// 	}
/// };
/// ```
///
#[derive(Debug, Clone)]
pub struct HumusEngineLoader<ApiFormat: HumusApiFormat> {
	/// The path to the directory where the templates are.
	pub template_location: PathBuf,

	/// The path to the extra configuration
	/// (relative to the current pwd, not to the templates)
	pub extra_config_location: Option<PathBuf>,

	/// The empty templaing engine.
	/// It is accessible so you can add customizations before loading the templates.
	pub tera: Tera,

	phantom_api_format: PhantomData<ApiFormat>,
}

impl<ApiFormat: HumusApiFormat> HumusEngineLoader<ApiFormat> {
	/// Creates a new `HumusEngineLoader` with minimal typing.
	pub fn new(template_location: PathBuf, extra_config_location: Option<PathBuf>) -> Self {
		Self {
			template_location: template_location,
			extra_config_location: extra_config_location,
			tera: Tera::new(),
			phantom_api_format: PhantomData,
		}
	}

	/// Overrides the template location with a new location if it is set.
	///
	/// Intended for processing cli-options.
	pub fn cli_template_location(mut self, location: Option<PathBuf>) -> Self {
		if let Some(location) = location {
			self.template_location = location;
		}
		self
	}

	/// Overrides the extra configuration location with a new location if it is set.
	///
	/// Intended for processing cli-options.
	pub fn cli_extra_config_location(mut self, location: Option<PathBuf>) -> Self {
		if let Some(location) = location {
			self.extra_config_location = Some(location);
		}
		self
	}

	/// Returns the template base directory.
	pub fn base_dir(&self) -> PathBuf {
		self.template_location.clone()
	}

	/// Initialize a [HumusEngine] with the given templates and extra configuration.
	///
	/// Failure Modes:
	/// * The `extra.toml` was not found and the path was explicitly set.
	/// * The `extra.toml` was found and is not valid toml.
	/// * The `extra.toml` passes, but tera finds an error in the templates.
	///
	/// If `extra_config_location` is `None` no error is returned if the `extra.toml`
	/// was not found as the template might not require one.
	#[expect(clippy::result_large_err)]
	pub fn load_templates(&self) -> Result<HumusEngine<ApiFormat>, HumusEngineLoaderError> {
		let languages_base_dir = self.template_location.join("languages");
		let language_engine = match LanguageEngine::load_from_directory(&languages_base_dir) {
			Ok(engine) => engine,
			Err(e) => {
				error!("Error loading language engine: {e}");
				match &e {
					LanguageEngineLoaderError::BaseDirectoryDoesNotExist { path, .. } => {
						warn!(
							"Using an empty language engine, localization functions will not work."
						);
						warn!(
							"Please consider creating the file {:?}, see the lib-humus crate documentation for details.",
							path.join("manifest.toml")
						);
						LanguageEngine::new_empty()
					}
					_ => {
						return Err(HumusEngineLoaderError::LanguageEngineError {
							path: languages_base_dir,
							error: e,
						});
					}
				}
			}
		};

		let language_engine = Arc::new(language_engine);

		let templating_engine = TemplatingEngine::load_from_directory(
			&self.template_location,
			self.extra_config_location.as_deref(),
			self.tera.clone(),
			Some(language_engine.clone()),
		)
		.map_err(HumusEngineLoaderError::TemplatingEngineError)?;

		Ok(HumusEngine::new(templating_engine, language_engine))
	}
}

/// Returned when loading a template using the [HumusEngineLoader] fails.
#[derive(Debug, thiserror::Error)]
pub enum HumusEngineLoaderError {
	/// Problem while loading templates
	#[error("Error loading templates: {0}")]
	TemplatingEngineError(#[source] TemplatingEngineLoaderError),
	/// An error occurred while loading languages
	#[error("Error loading languge engine {path:?}:\n{error}")]
	LanguageEngineError {
		/// Path to the languages directory
		path: PathBuf,
		/// what went wrong
		#[source]
		error: LanguageEngineLoaderError,
	},
}