tier 0.1.17

Rust configuration library for layered TOML, env, and CLI settings
Documentation
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ExternalPathSegment {
    Field(String),
    Index(String),
}

impl ExternalPathSegment {
    pub(crate) fn value(&self) -> &str {
        match self {
            Self::Field(value) | Self::Index(value) => value,
        }
    }
}

pub(crate) fn parse_external_path(path: &str) -> Result<Vec<ExternalPathSegment>, String> {
    if path == "." {
        return Ok(Vec::new());
    }

    let mut segments = Vec::new();
    let mut current = String::new();
    let mut chars = path.chars().peekable();
    let mut after_index = false;
    let mut expecting_segment = true;

    while let Some(ch) = chars.next() {
        if after_index {
            match ch {
                '.' => {
                    if chars.peek().is_none() {
                        return Err("configuration path cannot end with `.`".to_owned());
                    }
                    after_index = false;
                    expecting_segment = true;
                }
                '[' => {
                    let index = parse_array_index(&mut chars)?;
                    segments.push(ExternalPathSegment::Index(index));
                    after_index = true;
                    expecting_segment = false;
                }
                _ => {
                    return Err(
                        "expected `.` or `[` after an array index in configuration path".to_owned(),
                    );
                }
            }
            continue;
        }

        match ch {
            '.' => {
                if current.is_empty() {
                    return Err("empty path segment in configuration path".to_owned());
                }
                segments.push(ExternalPathSegment::Field(std::mem::take(&mut current)));
                expecting_segment = true;
            }
            '[' => {
                if current.is_empty() {
                    return Err("array indices must follow a field name".to_owned());
                }
                segments.push(ExternalPathSegment::Field(std::mem::take(&mut current)));
                let index = parse_array_index(&mut chars)?;
                segments.push(ExternalPathSegment::Index(index));
                after_index = true;
                expecting_segment = false;
            }
            ']' => return Err("unexpected `]` in configuration path".to_owned()),
            _ => {
                current.push(ch);
                expecting_segment = false;
            }
        }
    }

    if expecting_segment && !segments.is_empty() && current.is_empty() && !after_index {
        return Err("configuration path cannot end with `.`".to_owned());
    }

    if !current.is_empty() {
        segments.push(ExternalPathSegment::Field(current));
    }

    Ok(segments)
}

pub(crate) fn render_external_path(segments: &[ExternalPathSegment]) -> String {
    segments
        .iter()
        .map(ExternalPathSegment::value)
        .collect::<Vec<_>>()
        .join(".")
}

pub(crate) fn normalize_external_path(path: &str) -> Result<String, String> {
    parse_external_path(path).map(|segments| render_external_path(&segments))
}

fn parse_array_index<I>(chars: &mut std::iter::Peekable<I>) -> Result<String, String>
where
    I: Iterator<Item = char>,
{
    let mut index = String::new();
    let mut closed = false;
    for next in chars.by_ref() {
        if next == ']' {
            closed = true;
            break;
        }
        index.push(next);
    }
    if !closed {
        return Err("unclosed `[` in configuration path".to_owned());
    }
    if index.is_empty() {
        return Err("empty array index in configuration path".to_owned());
    }
    if !index.chars().all(|ch| ch.is_ascii_digit()) {
        return Err("array indices in configuration paths must be numeric".to_owned());
    }
    index
        .parse::<usize>()
        .map(|value| value.to_string())
        .map_err(|_| "array indices in configuration paths must fit in usize".to_owned())
}