lib-humus-configuration 0.2.0

Helper crate for reading configuration files into data structures using serde.
Documentation
// SPDX-FileCopyrightText: 2025 Slatian
//
// SPDX-License-Identifier: LGPL-3.0-or-later

use serde::Deserialize;

use std::fs;
use std::path::Path;

use crate::ConfigFormat;
use crate::ErrorCause;
use crate::HumusConfigError;
use crate::Settings;

/// Read from a given path using the given settings.
///
/// File type detection is based on file extension with the preferred type used as fallback for unknown extensions.
///
/// No type sniffing or fingerprinting is done.
pub fn read_from_file<T>(
	path: impl AsRef<Path>,
	settings: Settings,
) -> Result<(T, ConfigFormat), HumusConfigError>
where
	T: for<'de> Deserialize<'de>,
{
	let text = match fs::read_to_string(&path) {
		Ok(t) => t,
		Err(e) => {
			return Err(HumusConfigError {
				path: path.as_ref().into(),
				cause: ErrorCause::FileRead(e),
			});
		}
	};

	let path = path.as_ref();

	let detected_format = ConfigFormat::guess_from_path(path);
	if let Some(detected_format) = detected_format {
		if !settings.is_allowed(detected_format) {
			return Err(HumusConfigError {
				path: path.into(),
				cause: ErrorCause::FormatNotAllowed {
					format: detected_format,
					prefer: settings.prefer,
				},
			});
		}
	}

	match detected_format.unwrap_or(settings.prefer) {
		#[cfg(feature = "json")]
		ConfigFormat::Json => match serde_json::from_str(&text) {
			Ok(t) => Ok((t, ConfigFormat::Json)),
			Err(e) => Err(HumusConfigError {
				path: path.into(),
				cause: ErrorCause::Json(e),
			}),
		},
		#[cfg(feature = "json5")]
		ConfigFormat::Json5 => match json5::from_str(&text) {
			Ok(t) => Ok((t, ConfigFormat::Json5)),
			Err(e) => Err(HumusConfigError {
				path: path.into(),
				cause: ErrorCause::Json5(e),
			}),
		},
		#[cfg(feature = "toml")]
		ConfigFormat::Toml => match toml::from_str(&text) {
			Ok(t) => Ok((t, ConfigFormat::Toml)),
			Err(e) => Err(HumusConfigError {
				path: path.into(),
				cause: ErrorCause::Toml(e),
			}),
		},
	}
}

#[cfg(feature = "json")]
/// Read from a JSON file at a given path.
///
/// ```rust
/// use serde::Deserialize;
/// use lib_humus_configuration::read_from_json_file;
///
/// #[derive(Deserialize)]
/// struct TestData {
/// 	text: String,
/// 	n: i64,
/// }
///
/// let test_data: TestData = read_from_json_file("test-data/test.json").unwrap();
///
/// assert_eq!(test_data.text, "Foo Bar".to_string());
/// assert_eq!(test_data.n, 123);
/// ```
pub fn read_from_json_file<T>(path: impl AsRef<Path>) -> Result<T, HumusConfigError>
where
	T: for<'de> Deserialize<'de>,
{
	Ok(read_from_file(path, Settings::only_json())?.0)
}

#[cfg(feature = "json5")]
/// Read from a JSON5 or JSON file at a given path.
///
/// ```rust
/// use serde::Deserialize;
/// use lib_humus_configuration::read_from_json5_file;
///
/// #[derive(Deserialize)]
/// struct TestData {
/// 	text: String,
/// 	n: i64,
/// }
///
/// let test_data: TestData = read_from_json5_file("test-data/test.json5").unwrap();
///
/// assert_eq!(test_data.text, "Foo Bar".to_string());
/// assert_eq!(test_data.n, 123);
/// ```
pub fn read_from_json5_file<T>(path: impl AsRef<Path>) -> Result<T, HumusConfigError>
where
	T: for<'de> Deserialize<'de>,
{
	Ok(read_from_file(path, Settings::only_json5())?.0)
}

#[cfg(feature = "toml")]
/// Read from a TOML file at a given path.
///
/// ```rust
/// use serde::Deserialize;
/// use lib_humus_configuration::read_from_toml_file;
///
/// #[derive(Deserialize)]
/// struct TestData {
/// 	text: String,
/// 	n: i64,
/// }
///
/// let test_data: TestData = read_from_toml_file("test-data/test.toml").unwrap();
///
/// assert_eq!(test_data.text, "Foo Bar".to_string());
/// assert_eq!(test_data.n, 123);
/// ```
pub fn read_from_toml_file<T>(path: impl AsRef<Path>) -> Result<T, HumusConfigError>
where
	T: for<'de> Deserialize<'de>,
{
	Ok(read_from_file(path, Settings::only_toml())?.0)
}