ucifer 0.3.0

OpenWrt UCI Parser and Exporter
Documentation
/* Copyright © 2025 CZ.NIC z.s.p.o. (http://www.nic.cz/)
 *
 * This file is part of the ucifer library
 *
 * Ucifer is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * Ucifer is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * For more information, see /LICENSE.txt
 */

//! Configuration Option (includes lists) in a config section

use {
	alloc::{borrow::ToOwned, string::String, vec::Vec},
	core::fmt::Debug,
	derive_more::{Display, Error},
};

/// Configuration option in the section. This also includes lists.
#[derive(Clone, PartialEq, Eq, Hash)]
pub enum ConfigOption {
	/// A single option, delimited by `option`, not extended using `list`
	Scalar(String),
	/// Multiple options, either constructed as list directly or extended from
	/// `option`
	List(Vec<String>),
}

impl ConfigOption {
	/// Try interpret configuration option as bool
	///
	/// # Values
	/// - `true` <- 1, true, enabled, yes, on
	/// - `false` <- 0, false, disabled, no, off
	///
	/// # Example
	/// ```
	/// # use ucifer::document::ConfigOption;
	/// let is_true = ConfigOption::Scalar(String::from("on"));
	/// let is_false = ConfigOption::Scalar(String::from("disabled"));
	/// assert_eq!(is_true.try_as_bool(), Ok(true));
	/// assert_eq!(is_false.try_as_bool(), Ok(false));
	/// ```
	///
	/// # Errors
	/// - Option is not scalar
	/// - It's value doesn't match any of bools
	pub fn try_as_bool(&self) -> Result<bool, OptionVariantMismatch> {
		let s: &str = self.try_into()?;
		match s {
			"1" | "true" | "enabled" | "yes" | "on" => Ok(true),
			"0" | "false" | "disabled" | "no" | "off" => Ok(false),
			_ => Err(OptionVariantMismatch),
		}
	}
}

impl Debug for ConfigOption {
	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
		let inner: &dyn Debug = match self {
			Self::Scalar(x) => x as _,
			Self::List(x) => x,
		};

		Debug::fmt(inner, f)
	}
}

impl IntoIterator for ConfigOption {
	type Item = String;
	type IntoIter = IntoIter;

	fn into_iter(self) -> Self::IntoIter {
		IntoIter(match self {
			Self::Scalar(item) => AnyIterInner::Scalar(core::iter::once(item)),
			Self::List(items) => AnyIterInner::List(items.into_iter()),
		})
	}
}

impl<'a> IntoIterator for &'a ConfigOption {
	type Item = &'a str;
	type IntoIter = Iter<'a>;

	fn into_iter(self) -> Self::IntoIter {
		Iter(match self {
			ConfigOption::Scalar(item) => AnyIterInner::Scalar(core::iter::once(item)),
			ConfigOption::List(items) => AnyIterInner::List(items.iter()),
		})
	}
}

impl From<&str> for ConfigOption {
	fn from(value: &str) -> Self {
		Self::Scalar(value.to_owned())
	}
}

impl From<String> for ConfigOption {
	fn from(value: String) -> Self {
		Self::Scalar(value)
	}
}

impl From<Vec<String>> for ConfigOption {
	fn from(value: Vec<String>) -> Self {
		Self::List(value)
	}
}

impl From<&[String]> for ConfigOption {
	fn from(value: &[String]) -> Self {
		Self::List(value.to_owned())
	}
}

impl From<&[&str]> for ConfigOption {
	fn from(value: &[&str]) -> Self {
		Self::List(value.iter().copied().map(ToOwned::to_owned).collect())
	}
}

impl ConfigOption {
	/// Create a borrowing iterator
	///
	/// # Example
	/// ```
	/// # use ucifer::document::option::*;
	/// // We can iterate over a scalar option
	/// let option = ConfigOption::Scalar("single value".to_owned());
	/// let mut iter = option.iter();
	/// assert_eq!(iter.next(), Some("single value"));
	/// assert_eq!(iter.next(), None);
	///
	/// // …or over a list of options
	/// let options = ConfigOption::List(vec!["foo".to_owned(), "bar".to_owned()]);
	/// let mut iter = options.iter();
	/// assert_eq!(iter.next(), Some("foo"));
	/// assert_eq!(iter.next(), Some("bar"));
	/// assert_eq!(iter.next(), None);
	/// ```
	pub fn iter(&self) -> Iter<'_> {
		<&Self>::into_iter(self)
	}
}

/// Implements [`Iterator`] for [`ConfigOption`]
///
/// This is especially useful when we do not care about if the option is
/// single-item list or a scalar.
#[must_use]
pub struct Iter<'a>(AnyIterInner<&'a str, core::slice::Iter<'a, String>>);
impl<'a> Iterator for Iter<'a> {
	type Item = &'a str;

	fn next(&mut self) -> Option<Self::Item> {
		match &mut self.0 {
			AnyIterInner::Scalar(iter) => iter.next(),
			AnyIterInner::List(iter) => iter.next().map(String::as_str),
		}
	}
}

/// Implements [`IntoIterator`] for [`ConfigOption`]
///
/// This is especially useful when we do not care about if the option is
/// single-item list or a scalar.
#[must_use]
pub struct IntoIter(AnyIterInner<String, alloc::vec::IntoIter<String>>);
impl Iterator for IntoIter {
	type Item = String;

	fn next(&mut self) -> Option<Self::Item> {
		match &mut self.0 {
			AnyIterInner::Scalar(iter) => iter.next(),
			AnyIterInner::List(iter) => iter.next(),
		}
	}
}

/// Iterator implementation as there are multiple configuration variants
///
/// This is generic as this inner implementation is shared
enum AnyIterInner<Item, ListIter> {
	/// Once returns a scalar value
	Scalar(core::iter::Once<Item>),
	/// Iterator over the inner vector
	List(ListIter),
}

/// Invalid variant of [`ConfigOption`]
#[derive(Clone, Copy, Display, Debug, Error, PartialEq, Eq)]
#[display("expected variant doesn't match")]
pub struct OptionVariantMismatch;

impl TryFrom<ConfigOption> for String {
	type Error = OptionVariantMismatch;

	fn try_from(value: ConfigOption) -> Result<Self, Self::Error> {
		match value {
			ConfigOption::Scalar(s) => Ok(s),
			ConfigOption::List(_) => Err(OptionVariantMismatch),
		}
	}
}

impl TryFrom<ConfigOption> for Vec<String> {
	type Error = OptionVariantMismatch;

	fn try_from(value: ConfigOption) -> Result<Self, Self::Error> {
		match value {
			ConfigOption::List(l) => Ok(l),
			ConfigOption::Scalar(_) => Err(OptionVariantMismatch),
		}
	}
}

impl<'a> TryFrom<&'a ConfigOption> for &'a str {
	type Error = OptionVariantMismatch;

	fn try_from(value: &'a ConfigOption) -> Result<Self, Self::Error> {
		match value {
			ConfigOption::Scalar(s) => Ok(s),
			ConfigOption::List(_) => Err(OptionVariantMismatch),
		}
	}
}

impl<'a> TryFrom<&'a ConfigOption> for &'a [String] {
	type Error = OptionVariantMismatch;

	fn try_from(value: &'a ConfigOption) -> Result<Self, Self::Error> {
		match value {
			ConfigOption::List(l) => Ok(l),
			ConfigOption::Scalar(_) => Err(OptionVariantMismatch),
		}
	}
}