haematite-cli 0.21.3

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use super::{Input, Refused, Span, Spanned, Stage, first};

/// Read one top-level grouping field and retain its original byte positions.
pub(super) fn read(text: &str, start: usize) -> Result<Spanned<Stage>, Refused> {
    parse(text)
        .map(|mut parsed| {
            parsed.span.start += start;
            parsed.span.end += start;
            parsed
        })
        .map_err(|refusal| match refusal {
            Refused::Parse(message, span) => Refused::Parse(message, shifted(span, start)),
            Refused::WholeRecords(message, span) => {
                Refused::WholeRecords(message, shifted(span, start))
            }
            Refused::Percentile(message, span) => {
                Refused::Percentile(message, shifted(span, start))
            }
            Refused::NotYet(message, span) => Refused::NotYet(message, shifted(span, start)),
            Refused::Unserved(message, span) => Refused::Unserved(message, shifted(span, start)),
            Refused::Flag(message, span) => Refused::Flag(message, shifted(span, start)),
            Refused::Window(code, message, span) => {
                Refused::Window(code, message, shifted(span, start))
            }
        })
}

const fn shifted(span: Span, start: usize) -> Span {
    Span {
        start: span.start + start,
        end: span.end + start,
    }
}

fn parse(text: &str) -> Result<Spanned<Stage>, Refused> {
    let mut input = Input { text, rest: text };
    input.trim();
    let start = input.position();
    if input.word() != "group_by" {
        return Err(input.parse("`group_by` names the grouping stage"));
    }
    input.advance(8);
    input.require("(", "`group_by` takes one top-level field in brackets")?;
    input.require(
        ".",
        "`group_by` takes a field with a leading dot, as `.ward`",
    )?;
    let length = input
        .rest
        .find(|character| !first(character))
        .unwrap_or(input.rest.len());
    if length == 0 {
        return Err(input.parse("`group_by` needs a field name after its dot"));
    }
    let field = input.rest.split_at(length).0.to_owned();
    input.advance(length);
    input.require(
        ")",
        "`group_by` takes exactly one top-level field and a closing `)`",
    )?;
    Ok(Spanned {
        value: Stage::GroupBy { field },
        span: Span {
            start,
            end: input.position(),
        },
    })
}

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