cratestack-parser 0.3.6

Rust-native schema-first framework for typed HTTP APIs, generated clients, and backend services.
Documentation
use cratestack_core::{ConfigBlock, SourceSpan, TransportStyle};

use crate::diagnostics::SchemaError;
use crate::line_helpers::{Line, span_from_lines};

pub(super) fn parse_transport_directive(line: &Line<'_>) -> Result<TransportStyle, SchemaError> {
    let rest = line.trimmed.strip_prefix("transport").unwrap_or("").trim();
    if rest.is_empty() {
        return Err(SchemaError::new(
            "expected transport style after `transport` (one of: rest, rpc)",
            line.start..line.start + line.raw.len(),
            line.number,
        ));
    }
    match rest {
        "rest" => Ok(TransportStyle::Rest),
        "rpc" => Ok(TransportStyle::Rpc),
        other => Err(SchemaError::new(
            format!("unknown transport style `{other}` (expected one of: rest, rpc)"),
            line.start..line.start + line.raw.len(),
            line.number,
        )),
    }
}

pub(super) fn parse_named_config_block(
    lines: &[Line<'_>],
    start: usize,
    keyword: &str,
) -> Result<(ConfigBlock, usize), SchemaError> {
    let header = &lines[start];
    let prefix = format!("{keyword} ");
    let remainder = header.trimmed.strip_prefix(&prefix).ok_or_else(|| {
        SchemaError::new(
            format!("expected {keyword} declaration"),
            header.start..header.start + header.raw.len(),
            header.number,
        )
    })?;
    let name = remainder.strip_suffix('{').map(str::trim).ok_or_else(|| {
        SchemaError::new(
            format!("expected {keyword} block header ending with '{{'"),
            header.start..header.start + header.raw.len(),
            header.number,
        )
    })?;
    let (entries, next) = collect_block_entries(lines, start)?;

    Ok((
        ConfigBlock {
            docs: Vec::new(),
            name: name.to_owned(),
            entries,
            span: span_from_lines(header, &lines[next - 1]),
        },
        next,
    ))
}

pub(super) fn parse_simple_config_block(
    lines: &[Line<'_>],
    start: usize,
    keyword: &str,
) -> Result<(ConfigBlock, usize), SchemaError> {
    let header = &lines[start];
    if header.trimmed != format!("{keyword} {{") {
        return Err(SchemaError::new(
            format!("expected {keyword} block"),
            header.start..header.start + header.raw.len(),
            header.number,
        ));
    }

    let (entries, next) = collect_block_entries(lines, start)?;
    Ok((
        ConfigBlock {
            docs: Vec::new(),
            name: keyword.to_owned(),
            entries,
            span: span_from_lines(header, &lines[next - 1]),
        },
        next,
    ))
}

pub(super) fn parse_body_block<'a>(
    lines: &'a [Line<'a>],
    start: usize,
    keyword: &str,
) -> Result<(String, Vec<Line<'a>>, SourceSpan, usize), SchemaError> {
    let header = &lines[start];
    let prefix = format!("{keyword} ");
    let remainder = header.trimmed.strip_prefix(&prefix).ok_or_else(|| {
        SchemaError::new(
            format!("expected {keyword} declaration"),
            header.start..header.start + header.raw.len(),
            header.number,
        )
    })?;
    let name = remainder.strip_suffix('{').map(str::trim).ok_or_else(|| {
        SchemaError::new(
            format!("expected {keyword} block header ending with '{{'"),
            header.start..header.start + header.raw.len(),
            header.number,
        )
    })?;

    let mut body = Vec::new();
    let mut cursor = start + 1;
    while cursor < lines.len() {
        let line = &lines[cursor];
        if line.trimmed == "}" {
            return Ok((
                name.to_owned(),
                body,
                span_from_lines(header, line),
                cursor + 1,
            ));
        }
        body.push(line.clone());
        cursor += 1;
    }

    Err(SchemaError::new(
        format!("unterminated {keyword} block"),
        header.start..header.start + header.raw.len(),
        header.number,
    ))
}

fn collect_block_entries(
    lines: &[Line<'_>],
    start: usize,
) -> Result<(Vec<String>, usize), SchemaError> {
    let mut entries = Vec::new();
    let mut cursor = start + 1;
    while cursor < lines.len() {
        let line = &lines[cursor];
        if line.trimmed == "}" {
            return Ok((entries, cursor + 1));
        }
        if !line.trimmed.is_empty() && !line.trimmed.starts_with("//") {
            entries.push(line.trimmed.to_owned());
        }
        cursor += 1;
    }

    let header = &lines[start];
    Err(SchemaError::new(
        "unterminated config block",
        header.start..header.start + header.raw.len(),
        header.number,
    ))
}