#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConfigKeyError {
Empty,
EmptySegment,
DottedSegment,
}
impl fmt::Display for ConfigKeyError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::Empty => "configuration key is empty",
Self::EmptySegment => "configuration path contains an empty segment",
Self::DottedSegment => "configuration segment must not contain dots",
};
formatter.write_str(message)
}
}
impl Error for ConfigKeyError {}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ConfigKey(String);
impl ConfigKey {
pub fn new(input: impl AsRef<str>) -> Result<Self, ConfigKeyError> {
validated_single_segment(input.as_ref()).map(|segment| Self(segment.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl fmt::Display for ConfigKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl FromStr for ConfigKey {
type Err = ConfigKeyError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ConfigSection(String);
impl ConfigSection {
pub fn new(input: impl AsRef<str>) -> Result<Self, ConfigKeyError> {
validated_single_segment(input.as_ref()).map(|segment| Self(segment.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl fmt::Display for ConfigSection {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl FromStr for ConfigSection {
type Err = ConfigKeyError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ConfigPath {
segments: Vec<String>,
}
impl ConfigPath {
pub fn parse(input: &str) -> Result<Self, ConfigKeyError> {
input.parse()
}
pub fn from_segments<I, S>(segments: I) -> Result<Self, ConfigKeyError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut validated = Vec::new();
for segment in segments {
validated.push(validated_path_segment(segment.as_ref())?.to_owned());
}
if validated.is_empty() {
return Err(ConfigKeyError::Empty);
}
Ok(Self {
segments: validated,
})
}
#[must_use]
pub const fn len(&self) -> usize {
self.segments.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.segments.is_empty()
}
pub fn segments(&self) -> impl Iterator<Item = &str> {
self.segments.iter().map(String::as_str)
}
#[must_use]
pub fn get(&self, index: usize) -> Option<&str> {
self.segments.get(index).map(String::as_str)
}
#[must_use]
pub fn first(&self) -> Option<&str> {
self.get(0)
}
#[must_use]
pub fn last(&self) -> Option<&str> {
self.segments.last().map(String::as_str)
}
}
impl fmt::Display for ConfigPath {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, segment) in self.segments.iter().enumerate() {
if index > 0 {
formatter.write_str(".")?;
}
formatter.write_str(segment)?;
}
Ok(())
}
}
impl From<ConfigKey> for ConfigPath {
fn from(key: ConfigKey) -> Self {
Self {
segments: vec![key.into_string()],
}
}
}
impl FromStr for ConfigPath {
type Err = ConfigKeyError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(ConfigKeyError::Empty);
}
let mut segments = Vec::new();
for segment in trimmed.split('.') {
segments.push(validated_path_segment(segment)?.to_owned());
}
Ok(Self { segments })
}
}
fn validated_single_segment(input: &str) -> Result<&str, ConfigKeyError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(ConfigKeyError::Empty);
}
if trimmed.contains('.') {
return Err(ConfigKeyError::DottedSegment);
}
Ok(trimmed)
}
fn validated_path_segment(input: &str) -> Result<&str, ConfigKeyError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(ConfigKeyError::EmptySegment);
}
if trimmed.contains('.') {
return Err(ConfigKeyError::DottedSegment);
}
Ok(trimmed)
}
#[cfg(test)]
mod tests {
use super::{ConfigKey, ConfigKeyError, ConfigPath, ConfigSection};
use std::str::FromStr;
#[test]
fn valid_single_key() {
let key = ConfigKey::from_str(" port ").expect("key should parse");
assert_eq!(key.as_str(), "port");
assert_eq!(key.to_string(), "port");
}
#[test]
fn valid_dotted_path() {
let path = ConfigPath::parse("server.port").expect("path should parse");
assert_eq!(path.len(), 2);
assert_eq!(path.first(), Some("server"));
assert_eq!(path.last(), Some("port"));
}
#[test]
fn invalid_empty_key() {
assert_eq!(ConfigKey::new(" "), Err(ConfigKeyError::Empty));
assert_eq!(ConfigSection::new(""), Err(ConfigKeyError::Empty));
assert_eq!(ConfigPath::parse(""), Err(ConfigKeyError::Empty));
}
#[test]
fn invalid_empty_segment() {
assert_eq!(
ConfigPath::parse("server..port"),
Err(ConfigKeyError::EmptySegment)
);
}
#[test]
fn segment_iteration_and_access_preserve_order() {
let path = ConfigPath::parse("server.http.port").expect("path should parse");
let segments: Vec<_> = path.segments().collect();
assert_eq!(segments, vec!["server", "http", "port"]);
assert_eq!(path.get(1), Some("http"));
}
#[test]
fn display_round_trip() {
let path = ConfigPath::parse("server.port").expect("path should parse");
let round_trip = ConfigPath::parse(&path.to_string()).expect("display should parse");
assert_eq!(path, round_trip);
}
}