ctl-core 0.7.4

A framework for clap programs: typed results, help, and terminal or JSON output
Documentation
//! The TOML half of [`Input`]. It parses with the `toml` crate's
//! span-carrying tree, holds the tree to the limits, and places each problem
//! on the value it names.
//!
//! Every position here is a byte span in the text, turned into a 1-based line
//! and column once, in [`position`].

use std::ops::Range;

use serde::de::DeserializeOwned;
use toml::Spanned;
use toml::de::{DeTable, DeValue, Deserializer};

use super::limits::{Bounds, Exceeded, Raise};
use super::{At, FieldPath, Input, InputError, Problem, Step};

/// [`Input::parse`] for a TOML text: the tree, then the limits, then the
/// typed value. The first problem found is the one reported.
pub(super) fn parse<T: DeserializeOwned>(
    input: &Input,
    bounds: Bounds,
    raise: Raise,
) -> Result<T, InputError> {
    let root = DeTable::parse(&input.text)
        .map_err(|error| refusal(input, error.span(), error.message()))?;
    let mut measure = Measure {
        max_depth: bounds.depth,
        max_nodes: bounds.nodes,
        nodes: 0,
        raise,
    };
    measure
        .table(&root.span(), root.get_ref(), 1)
        .map_err(|refused| refusal(input, Some(refused.at), refused.exceeded.to_string()))?;
    T::deserialize(Deserializer::from(root))
        .map_err(|error| refusal(input, error.span(), error.message()))
}

/// The 1-based line and column of each path in `text`, or of its nearest
/// written ancestor. `None` for every path when `text` does not parse.
pub(super) fn places(text: &str, paths: &[FieldPath], at: At) -> Vec<Option<(usize, usize)>> {
    let root = DeTable::parse(text)
        .ok()
        .map(|table| Spanned::new(table.span(), DeValue::Table(table.into_inner())));
    paths
        .iter()
        .map(|path| {
            root.as_ref()
                .map(|root| position(text, nearest(root, path.steps(), at).start))
        })
        .collect()
}

/// The span of the value `steps` reach, or of the last one that exists, or with
/// [`At::Key`] the span of the key that names it. An item and the root have no
/// key, so they give their value.
fn nearest(root: &Spanned<DeValue<'_>>, steps: &[Step], at: At) -> Range<usize> {
    let mut found = root;
    let mut key = None;
    for step in steps {
        let next = match (found.get_ref(), step) {
            (DeValue::Table(table), Step::Key(name)) => table
                .iter()
                .find(|(entry, _)| entry.get_ref().as_ref() == name.as_str())
                .map(|(entry, value)| (value, Some(entry.span()))),
            (value, Step::Key(name)) => value.get(name.as_str()).map(|value| (value, None)),
            (value, Step::Index(index)) => value.get(*index).map(|value| (value, None)),
        };
        let Some((next, named)) = next else { break };
        (found, key) = (next, named);
    }
    match at {
        At::Value => found.span(),
        At::Key => key.unwrap_or_else(|| found.span()),
    }
}

/// A problem with the whole input, placed at `span` when the parser gave one.
fn refusal(input: &Input, span: Option<Range<usize>>, message: impl Into<String>) -> InputError {
    let place = span.map(|span| position(&input.text, span.start));
    InputError::of(
        input.name.clone(),
        vec![Problem {
            line: place.map(|(line, _)| line + input.lines_before),
            column: place.map(|(_, column)| column),
            field: String::new(),
            message: message.into(),
        }],
    )
}

/// The 1-based line and column, counted in characters, of byte `offset` in
/// `text`. An offset past the end, as at an unclosed array, is the end.
fn position(text: &str, offset: usize) -> (usize, usize) {
    let before = String::from_utf8_lossy(&text.as_bytes()[..offset.min(text.len())]);
    let line = before.split('\n').count();
    let column = 1 + before
        .rsplit('\n')
        .next()
        .map_or(0, |last| last.chars().count());
    (line, column)
}

/// A limit a tree went past, and where in the text.
struct Refused {
    exceeded: Exceeded,
    at: Range<usize>,
}

/// Walks a tree in the order it was written, counting depth and nodes
/// against the limits in force. `max_depth` is YAML's rule too: a table or
/// an array is one level, and the root table is the first.
struct Measure {
    max_depth: usize,
    max_nodes: usize,
    nodes: usize,
    raise: Raise,
}

impl Measure {
    /// A table at `depth`, then its entries in the order they were written:
    /// a parsed table iterates by key, not by position.
    fn table(
        &mut self,
        span: &Range<usize>,
        table: &DeTable<'_>,
        depth: usize,
    ) -> Result<(), Refused> {
        self.container(span, depth)?;
        let mut entries: Vec<_> = table.iter().collect();
        entries.sort_by_key(|(key, _)| key.span().start);
        entries
            .into_iter()
            .try_for_each(|(_, value)| self.value(value, depth + 1))
    }

    fn value(&mut self, value: &Spanned<DeValue<'_>>, depth: usize) -> Result<(), Refused> {
        let span = value.span();
        match value.get_ref() {
            DeValue::Table(table) => self.table(&span, table, depth),
            DeValue::Array(items) => {
                self.container(&span, depth)?;
                items
                    .iter()
                    .try_for_each(|item| self.value(item, depth + 1))
            }
            DeValue::String(_)
            | DeValue::Integer(_)
            | DeValue::Float(_)
            | DeValue::Boolean(_)
            | DeValue::Datetime(_) => self.node(&span),
        }
    }

    /// A table or an array: one level deeper, and one node.
    fn container(&mut self, span: &Range<usize>, depth: usize) -> Result<(), Refused> {
        if depth > self.max_depth {
            return Err(self.refuse("max_depth", self.max_depth, span));
        }
        self.node(span)
    }

    fn node(&mut self, span: &Range<usize>) -> Result<(), Refused> {
        self.nodes += 1;
        if self.nodes > self.max_nodes {
            return Err(self.refuse("max_nodes", self.max_nodes, span));
        }
        Ok(())
    }

    fn refuse(&self, field: &'static str, limit: usize, span: &Range<usize>) -> Refused {
        Refused {
            exceeded: Exceeded::new(field, limit, self.raise),
            at: span.clone(),
        }
    }
}