tier 0.1.17

Rust configuration library for layered TOML, env, and CLI settings
Documentation
#[derive(Debug, Clone, PartialEq, Eq)]
struct DelimitedField {
    value: String,
    quoted: bool,
}

pub(super) fn parse_csv_fields(raw: &str) -> Result<Vec<String>, String> {
    parse_delimited_fields(raw, ',').map(filter_empty_unquoted_fields)
}

pub(super) fn parse_key_value_fields(raw: &str) -> Result<Vec<String>, String> {
    parse_key_value_delimited_fields(raw, ',').map(filter_empty_unquoted_fields)
}

fn filter_empty_unquoted_fields(fields: Vec<DelimitedField>) -> Vec<String> {
    fields
        .into_iter()
        .filter(|field| field.quoted || !field.value.is_empty())
        .map(|field| field.value)
        .collect()
}

fn parse_delimited_fields(raw: &str, delimiter: char) -> Result<Vec<DelimitedField>, String> {
    let mut fields = Vec::new();
    let mut chars = raw.chars().peekable();

    loop {
        consume_horizontal_whitespace(&mut chars);
        if chars.peek().is_none() {
            break;
        }

        let field = if chars.peek() == Some(&'"') {
            chars.next();
            let value = parse_quoted_field(&mut chars, "csv")?;
            consume_horizontal_whitespace(&mut chars);
            DelimitedField {
                value,
                quoted: true,
            }
        } else {
            DelimitedField {
                value: parse_unquoted_field(&mut chars, delimiter)
                    .trim()
                    .to_owned(),
                quoted: false,
            }
        };
        fields.push(field);

        match chars.peek() {
            Some(ch) if *ch == delimiter => {
                chars.next();
            }
            Some(ch) => {
                return Err(format!(
                    "invalid csv field separator `{ch}`, expected `{delimiter}`"
                ));
            }
            None => break,
        }
    }

    Ok(fields)
}

fn parse_key_value_delimited_fields(
    raw: &str,
    delimiter: char,
) -> Result<Vec<DelimitedField>, String> {
    let mut fields = Vec::new();
    let mut chars = raw.chars().peekable();
    let mut value = String::new();
    let mut quoted = false;

    while let Some(ch) = chars.next() {
        if ch == delimiter {
            fields.push(DelimitedField {
                value: value.trim().to_owned(),
                quoted,
            });
            value.clear();
            quoted = false;
            continue;
        }

        if ch == '"' {
            quoted = true;
            value.push_str(&parse_quoted_field(&mut chars, "key_value_map")?);
            continue;
        }

        value.push(ch);
    }

    fields.push(DelimitedField {
        value: value.trim().to_owned(),
        quoted,
    });

    Ok(fields)
}

fn parse_quoted_field<I>(
    chars: &mut std::iter::Peekable<I>,
    context: &str,
) -> Result<String, String>
where
    I: Iterator<Item = char>,
{
    let mut value = String::new();
    loop {
        match chars.next() {
            Some('"') if chars.peek() == Some(&'"') => {
                chars.next();
                value.push('"');
            }
            Some('"') => return Ok(value),
            Some(ch) => value.push(ch),
            None => return Err(format!("unterminated quoted {context} field")),
        }
    }
}

fn parse_unquoted_field<I>(chars: &mut std::iter::Peekable<I>, delimiter: char) -> String
where
    I: Iterator<Item = char>,
{
    let mut value = String::new();
    while let Some(ch) = chars.peek().copied() {
        if ch == delimiter {
            break;
        }
        chars.next();
        value.push(ch);
    }
    value
}

fn consume_horizontal_whitespace<I>(chars: &mut std::iter::Peekable<I>)
where
    I: Iterator<Item = char>,
{
    while matches!(chars.peek(), Some(' ' | '\t')) {
        chars.next();
    }
}