cratestack-parser 0.7.3

Rust-native schema-first framework for typed HTTP APIs, generated clients, and backend services.
Documentation
use chumsky::prelude::*;
use cratestack_core::{SourceSpan, TypeArity, TypeRef};

use crate::diagnostics::SchemaError;
use crate::line_helpers::Line;

pub(super) fn parse_type_ref(
    raw: &str,
    line: &Line<'_>,
    raw_offset: usize,
) -> Result<TypeRef, SchemaError> {
    // `Vector(1536)` is the only parametric scalar today (see
    // `docs/design/extensions.md` §6): an identifier optionally
    // followed by one compile-time integer literal in parens, then
    // the usual arity suffix. The parenthesized argument is generic
    // in the grammar (any ident may carry one) — `validate_type_ref`
    // in `cratestack-parser::validate` is what actually restricts
    // parametric arguments to `Vector`.
    let parser = text::ident::<_, extra::Err<Simple<char>>>()
        .then(
            just('(')
                .ignore_then(text::int::<_, extra::Err<Simple<char>>>(10))
                .then_ignore(just(')'))
                .or_not(),
        )
        .then(choice((
            just("[]").to(TypeArity::List),
            just("?").to(TypeArity::Optional),
            end().to(TypeArity::Required),
        )))
        .then_ignore(end());

    parser
        .parse(raw)
        .into_result()
        .ok()
        .and_then(|((name, dimension), arity)| {
            let int_args = match dimension {
                Some(digits) => vec![digits.parse::<u32>().ok()?],
                None => Vec::new(),
            };
            Some(TypeRef {
                name: name.to_owned(),
                name_span: SourceSpan {
                    start: line.start + raw_offset,
                    end: line.start + raw_offset + name.len(),
                    line: line.number,
                },
                arity,
                generic_args: Vec::new(),
                int_args,
            })
        })
        .ok_or(())
        .or_else(|()| parse_builtin_generic_type_ref(raw, line, raw_offset))
        .map_err(|()| {
            SchemaError::new(
                format!("invalid type reference: {raw}"),
                line.start..line.start + line.raw.len(),
                line.number,
            )
        })
}

/// Built-in generic type names — every one of these accepts exactly one
/// `<T>` argument in this grammar today (`Page<T>` for procedure return
/// types, `FindMany<T>` for procedure arguments). Add new entries here as
/// new generic builtins are introduced; the parsing logic below is
/// otherwise fully shared.
const GENERIC_BUILTIN_NAMES: &[&str] = &["Page", "FindMany"];

fn parse_builtin_generic_type_ref(
    raw: &str,
    line: &Line<'_>,
    raw_offset: usize,
) -> Result<TypeRef, ()> {
    let (base, arity) = if let Some(base) = raw.strip_suffix("[]") {
        (base.trim(), TypeArity::List)
    } else if let Some(base) = raw.strip_suffix('?') {
        (base.trim(), TypeArity::Optional)
    } else {
        (raw.trim(), TypeArity::Required)
    };

    let name = GENERIC_BUILTIN_NAMES
        .iter()
        .copied()
        .find(|name| {
            base.strip_prefix(name)
                .is_some_and(|rest| rest.starts_with('<'))
        })
        .ok_or(())?;

    let inner = base[name.len() + 1..].strip_suffix('>').ok_or(())?;

    let inner_offset = base.find('<').ok_or(())? + 1;
    let inner_ref =
        parse_type_ref(inner.trim(), line, raw_offset + inner_offset).map_err(|_| ())?;
    Ok(TypeRef {
        name: name.to_owned(),
        name_span: SourceSpan {
            start: line.start + raw_offset,
            end: line.start + raw_offset + name.len(),
            line: line.number,
        },
        arity,
        generic_args: vec![inner_ref],
        int_args: Vec::new(),
    })
}