this-me 0.3.3

Rust ground for the modern .me semantic kernel.
Documentation
use std::fmt;

pub type Path = Vec<String>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedPath {
    parts: Vec<PathPart>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathPart {
    Segment(String),
    Selector(Selector),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Selector {
    EmptyPlural,
    Literal(String),
    Expression(String),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathParseError {
    Empty,
    EmptySegment,
    UnclosedSelector,
    UnterminatedQuote,
}

impl fmt::Display for PathParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => write!(f, "path cannot be empty"),
            Self::EmptySegment => write!(f, "path segment cannot be empty"),
            Self::UnclosedSelector => write!(f, "selector is missing a closing bracket"),
            Self::UnterminatedQuote => write!(f, "selector quote is not terminated"),
        }
    }
}

impl std::error::Error for PathParseError {}

impl ParsedPath {
    pub fn parse(input: &str) -> Result<Self, PathParseError> {
        let input = input.trim();
        if input.is_empty() {
            return Err(PathParseError::Empty);
        }

        let mut parts = Vec::new();
        let mut segment = String::new();
        let mut last_was_dot = true;
        let mut i = 0;

        while i < input.len() {
            let ch = next_char(input, i);
            match ch {
                '.' => {
                    if segment.trim().is_empty() {
                        if last_was_dot {
                            return Err(PathParseError::EmptySegment);
                        }
                    } else {
                        flush_segment(&mut parts, &mut segment)?;
                    }
                    last_was_dot = true;
                    i += ch.len_utf8();
                }
                '[' => {
                    if !segment.trim().is_empty() {
                        flush_segment(&mut parts, &mut segment)?;
                    }
                    let (selector, next) = parse_selector(input, i + ch.len_utf8())?;
                    parts.push(PathPart::Selector(selector));
                    last_was_dot = false;
                    i = next;
                }
                _ => {
                    segment.push(ch);
                    last_was_dot = false;
                    i += ch.len_utf8();
                }
            }
        }

        if !segment.trim().is_empty() {
            flush_segment(&mut parts, &mut segment)?;
        } else if last_was_dot {
            return Err(PathParseError::EmptySegment);
        }

        if parts.is_empty() {
            return Err(PathParseError::Empty);
        }

        Ok(Self { parts })
    }

    pub fn parts(&self) -> &[PathPart] {
        &self.parts
    }

    pub fn normalized(&self) -> Path {
        let mut out = Vec::new();
        for part in &self.parts {
            match part {
                PathPart::Segment(segment) => out.push(segment.clone()),
                PathPart::Selector(Selector::EmptyPlural) => {}
                PathPart::Selector(Selector::Literal(value))
                | PathPart::Selector(Selector::Expression(value)) => out.push(value.clone()),
            }
        }
        out
    }
}

pub trait IntoPath {
    fn into_path(self) -> Result<Path, PathParseError>;
}

impl IntoPath for Path {
    fn into_path(self) -> Result<Path, PathParseError> {
        Ok(self)
    }
}

impl IntoPath for &[&str] {
    fn into_path(self) -> Result<Path, PathParseError> {
        Ok(self.iter().map(|segment| (*segment).to_string()).collect())
    }
}

impl<const N: usize> IntoPath for [&str; N] {
    fn into_path(self) -> Result<Path, PathParseError> {
        Ok(self.into_iter().map(str::to_string).collect())
    }
}

impl IntoPath for &str {
    fn into_path(self) -> Result<Path, PathParseError> {
        if self.trim().is_empty() {
            return Ok(Vec::new());
        }
        ParsedPath::parse(self).map(|path| path.normalized())
    }
}

impl IntoPath for String {
    fn into_path(self) -> Result<Path, PathParseError> {
        self.as_str().into_path()
    }
}

fn flush_segment(parts: &mut Vec<PathPart>, segment: &mut String) -> Result<(), PathParseError> {
    let segment = std::mem::take(segment).trim().to_string();
    if segment.is_empty() {
        return Err(PathParseError::EmptySegment);
    }
    parts.push(PathPart::Segment(segment));
    Ok(())
}

fn parse_selector(input: &str, start: usize) -> Result<(Selector, usize), PathParseError> {
    let mut raw = String::new();
    let mut quote = None;
    let mut escaped = false;
    let mut i = start;

    while i < input.len() {
        let ch = next_char(input, i);
        if escaped {
            raw.push(ch);
            escaped = false;
            i += ch.len_utf8();
            continue;
        }

        if let Some(expected_quote) = quote {
            if ch == '\\' {
                raw.push(ch);
                escaped = true;
            } else {
                if ch == expected_quote {
                    quote = None;
                }
                raw.push(ch);
            }
            i += ch.len_utf8();
            continue;
        }

        match ch {
            '"' | '\'' => {
                quote = Some(ch);
                raw.push(ch);
                i += ch.len_utf8();
            }
            ']' => {
                if quote.is_some() {
                    return Err(PathParseError::UnterminatedQuote);
                }
                return Ok((classify_selector(&raw)?, i + ch.len_utf8()));
            }
            _ => {
                raw.push(ch);
                i += ch.len_utf8();
            }
        }
    }

    if quote.is_some() {
        return Err(PathParseError::UnterminatedQuote);
    }
    Err(PathParseError::UnclosedSelector)
}

fn classify_selector(raw: &str) -> Result<Selector, PathParseError> {
    let selector = raw.trim();
    if selector.is_empty() {
        return Ok(Selector::EmptyPlural);
    }

    if is_quoted(selector) {
        return Ok(Selector::Literal(unquote(selector)?));
    }

    if looks_like_expression(selector) {
        return Ok(Selector::Expression(selector.to_string()));
    }

    Ok(Selector::Literal(selector.to_string()))
}

fn is_quoted(value: &str) -> bool {
    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        return false;
    };
    let Some(last) = value.chars().last() else {
        return false;
    };
    (first == '"' || first == '\'') && first == last && value.len() >= 2
}

fn unquote(value: &str) -> Result<String, PathParseError> {
    let mut chars = value.chars();
    let quote = chars.next().ok_or(PathParseError::UnterminatedQuote)?;
    let mut out = String::new();
    let mut escaped = false;

    for ch in chars.take(value.chars().count().saturating_sub(2)) {
        if escaped {
            out.push(ch);
            escaped = false;
            continue;
        }
        if ch == '\\' {
            escaped = true;
        } else {
            out.push(ch);
        }
    }

    if escaped {
        out.push('\\');
    }

    if !value.ends_with(quote) {
        return Err(PathParseError::UnterminatedQuote);
    }

    Ok(out)
}

fn looks_like_expression(selector: &str) -> bool {
    selector.contains("=>")
        || selector.contains("&&")
        || selector.contains("||")
        || selector.contains(">=")
        || selector.contains("<=")
        || selector.contains("==")
        || selector.contains("!=")
        || selector.contains('>')
        || selector.contains('<')
        || selector.contains("..")
}

fn next_char(input: &str, index: usize) -> char {
    input[index..]
        .chars()
        .next()
        .expect("index must point at a valid character")
}