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 fluent::FluentValue;
use fluent::types::{FluentNumber, FluentNumberOptions, FluentNumberStyle, FluentNumberType};
use serde::{Deserialize, Serialize};

/// Describes a single variable that can be passed to a fleunt template
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariableDescription {
	/// Weather the variable is optional or required
	#[serde(default)]
	pub optional: bool,
	/// In cases of enum like values, this is a list of allowed values
	#[serde(default)]
	pub one_of: Option<Vec<Variable>>,
	/// The default value to set this to when not
	#[serde(default)]
	pub default_value: Option<Variable>,
	/// The variable type
	#[serde(rename = "type")]
	pub typ: VariableType,

	/// Weather to use grouping for numbers
	#[serde(default)]
	pub use_grouping: bool,
	/// Number setting
	#[serde(default)]
	pub minimum_integer_digits: Option<usize>,
	/// Number setting
	#[serde(default)]
	pub minimum_fraction_digits: Option<usize>,
	/// Number setting
	#[serde(default)]
	pub maximum_fraction_digits: Option<usize>,
	/// Number setting
	#[serde(default)]
	pub minimum_significant_digits: Option<usize>,
	/// Number setting
	#[serde(default)]
	pub maximum_significant_digits: Option<usize>,
}

impl VariableDescription {
	/// Returns weather the given variable matches the description
	///
	/// TODO: return an error here to allow ofr better error messages on mismatch
	pub fn matches_variable(&self, var: &Variable) -> bool {
		if var.get_affinity() != self.typ.get_affinity() {
			return false;
		}
		if let Some(list) = &self.one_of {
			for test in list {
				if var == test {
					return true;
				}
			}
			return false;
		}
		return true;
	}
}

/// Represents the kind of variable in a way that also singals the inteded usecase
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VariableType {
	/// Text to be used as is or as configuration
	Text,
	/// A boolean inteded for configuration
	Bool,
	/// A number in the general sense
	Cardinal,
	/// A number for counting something i.e. first, second, third, …
	Ordinal,
	/// A generic number type that works better with number matching blocks to work around
	/// https://github.com/projectfluent/fluent-rs/issues/401
	DefaultNumber,
	/// A number representing some fraction in percent
	Percent,
	// TODO: currency
}

impl VariableType {
	/// Returns the underlying datatype that is required to fill this variable type with content
	pub fn get_affinity(&self) -> VariableAffinity {
		match self {
			Self::Text => VariableAffinity::Text,
			Self::Bool => VariableAffinity::Bool,
			Self::Percent | Self::Cardinal | Self::Ordinal | Self::DefaultNumber => {
				VariableAffinity::Number
			}
		}
	}
}

/// Represents the datatype under a specific variable type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VariableAffinity {
	/// for strings
	Text,
	/// for f64 numbers
	Number,
	/// for booleans
	Bool,
}

/// A value that can be passed to a fluent template
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Variable {
	/// Represents a textual value
	Text(String),
	/// Represents a number value
	Number(f64),
	/// Represents a boolean value
	Bool(bool),
}

impl Variable {
	/// Returns which basic type this value represents
	pub fn get_affinity(&self) -> VariableAffinity {
		match self {
			Self::Text(_) => VariableAffinity::Text,
			Self::Number(_) => VariableAffinity::Number,
			Self::Bool(_) => VariableAffinity::Bool,
		}
	}

	/// Uses this value and its description to return a cnfigured fluent value
	pub fn into_fluent_value(self, description: &VariableDescription) -> FluentValue<'static> {
		match self {
			Self::Text(text) => FluentValue::String(text.into()),
			Self::Bool(true) => FluentValue::String("true".into()),
			Self::Bool(false) => FluentValue::String("false".into()),
			Self::Number(n) if matches!(description.typ, VariableType::DefaultNumber) => {
				FluentValue::Number(FluentNumber {
					value: n,
					options: Default::default(),
				})
			}
			Self::Number(n) => FluentValue::Number(FluentNumber {
				value: n,
				options: FluentNumberOptions {
					r#type: if matches!(description.typ, VariableType::Ordinal) {
						FluentNumberType::Ordinal
					} else {
						FluentNumberType::Cardinal
					},
					style: if matches!(description.typ, VariableType::Percent) {
						FluentNumberStyle::Percent
					} else {
						FluentNumberStyle::Decimal
					},
					currency: None,
					currency_display: Default::default(),
					use_grouping: description.use_grouping,
					minimum_integer_digits: description.minimum_integer_digits,
					minimum_fraction_digits: description.minimum_fraction_digits,
					maximum_fraction_digits: description.maximum_fraction_digits,
					minimum_significant_digits: description.minimum_significant_digits,
					maximum_significant_digits: description.maximum_significant_digits,
				},
			}),
		}
	}
}