haematite-cli 0.21.1

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! Read rg arguments without consuming a following pipeline stage.

use std::ops::Range;

use haematite_query::stage::rg::Flags;

use super::{Input, Refused, Span, Spanned, Stage, first};

/// The shared dispatcher advances by the returned span's byte length.
pub(super) fn read(text: &str, start: usize) -> Result<Spanned<Stage>, Refused> {
    let mut input = Input { text, rest: text };
    let result = arguments(&mut input).map(|value| Spanned {
        value,
        span: Span {
            start,
            end: start + input.position(),
        },
    });
    result.map_err(|refused| shifted(refused, start))
}

fn arguments(input: &mut Input<'_>) -> Result<Stage, Refused> {
    input.trim();
    let name = input.word();
    if !matches!(name, "rg" | "grep") {
        return Err(input.parse("an rg stage starts with `rg`"));
    }
    input.advance(name.len());
    input.require("(", "`rg` takes its arguments in brackets")?;
    input.trim();
    let field = if input.take(".") {
        let length = input
            .rest
            .find(|character| !first(character))
            .unwrap_or(input.rest.len());
        if length == 0 {
            return Err(input.parse("an rg field has a name after its dot"));
        }
        let field = input.rest.split_at(length).0.to_owned();
        input.advance(length);
        input.require(";", "an rg field is followed by `;` and a quoted pattern")?;
        Some(field)
    } else {
        None
    };
    let pattern = input.string("an rg pattern")?;
    input.trim();
    let flags = if input.take(";") {
        input.trim();
        let start = input.position();
        let literal = input.rest;
        let flags = input.string("rg flags")?;
        let (literal, _) = literal.split_at(input.position() - start);
        if let Err(refusal) = Flags::parse_spanned(&flags) {
            let span = flag_span(literal, &flags, refusal.span, start)?;
            return Err(Refused::Flag(refusal.message, span));
        }
        flags
    } else {
        String::new()
    };
    input.require(
        ")",
        "`rg(` ends after its pattern and optional flags with `)`",
    )?;
    Ok(Stage::Rg {
        field,
        pattern,
        flags,
    })
}

fn flag_span(
    literal: &str,
    decoded: &str,
    range: Range<usize>,
    offset: usize,
) -> Result<Span, Refused> {
    let invalid = || {
        Refused::Parse(
            "rg flag span cannot be mapped to its quoted string".to_owned(),
            Span {
                start: offset,
                end: offset + literal.len(),
            },
        )
    };
    let mut written = literal.char_indices().skip(1);
    let mut start = None;
    let mut end = None;
    for (index, character) in decoded.char_indices() {
        let (raw_start, raw) = written.next().ok_or_else(invalid)?;
        let mut raw_end = raw_start + raw.len_utf8();
        if raw == '\\' {
            let (position, escape) = written.next().ok_or_else(invalid)?;
            raw_end = position + escape.len_utf8();
            if escape == 'u' {
                // A decoded scalar above the BMP consumes both surrogate escapes.
                let count = if character.len_utf16() == 2 { 10 } else { 4 };
                for _ in 0..count {
                    let (position, character) = written.next().ok_or_else(invalid)?;
                    raw_end = position + character.len_utf8();
                }
            }
        }
        if index == range.start {
            start = Some(offset + raw_start);
        }
        if index + character.len_utf8() == range.end {
            end = Some(offset + raw_end);
            break;
        }
    }
    Ok(Span {
        start: start.ok_or_else(invalid)?,
        end: end.ok_or_else(invalid)?,
    })
}

fn shifted(refused: Refused, offset: usize) -> Refused {
    let span = refused.span();
    let span = Span {
        start: span.start + offset,
        end: span.end + offset,
    };
    match refused {
        Refused::NotYet(name, _) => Refused::NotYet(name, span),
        Refused::Unserved(message, _) => Refused::Unserved(message, span),
        Refused::Parse(message, _) => Refused::Parse(message, span),
        Refused::WholeRecords(message, _) => Refused::WholeRecords(message, span),
        Refused::Percentile(message, _) => Refused::Percentile(message, span),
        Refused::Window(code, message, _) => Refused::Window(code, message, span),
        Refused::Flag(message, _) => Refused::Flag(message, span),
    }
}

#[cfg(test)]
#[path = "rg_tests.rs"]
mod rg_tests;