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
//! Percentile grammar stays beside its lane-owned runner files.

use serde_json::Number;

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

pub(super) fn read(input: &mut Input<'_>) -> Result<Stage, Refused> {
    input.advance(10);
    input.require("(", "`percentile` takes a field and number in brackets")?;
    input.require(
        ".",
        "`percentile` takes a top-level field with a leading dot",
    )?;
    let length = input
        .rest
        .find(|character| !first(character))
        .unwrap_or(input.rest.len());
    if length == 0 {
        return Err(input.parse("`percentile` needs a field name after its dot"));
    }
    let field = input.rest.split_at(length).0.to_owned();
    input.advance(length);
    input.require(";", "`percentile` separates its field and number with `;`")?;
    input.trim();
    let Some((literal, _)) = input.rest.split_once(')') else {
        return Err(input.parse("`percentile` needs a number and a closing `)`"));
    };
    let number = literal.trim_end();
    let span = Span {
        start: input.position(),
        end: input.position() + number.len(),
    };
    let percentile: Number = serde_json::from_str(number).map_err(|error| {
        Refused::Parse(format!("`percentile` takes a JSON number: {error}"), span)
    })?;
    haematite_query::stage::percentile::Percentile::compile(field.clone(), percentile.clone())
        .map_err(|error| Refused::Percentile(error.refusal.message, span))?;
    input.advance(literal.len() + 1);
    Ok(Stage::Percentile { field, percentile })
}

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