cratestack-parser 0.3.7

Rust-native schema-first framework for typed HTTP APIs, generated clients, and backend services.
Documentation
use std::collections::BTreeMap;

use cratestack_core::{Attribute, Procedure, ProcedureArg, ProcedureKind, SourceSpan};

use crate::diagnostics::SchemaError;
use crate::line_helpers::{Line, name_span_in_line, trimmed_span};
use crate::parse::procedure_docs::split_procedure_docs;
use crate::parse::types::parse_type_ref;

pub(super) fn parse_procedure(
    lines: &[Line<'_>],
    start: usize,
    docs: Vec<String>,
) -> Result<(Procedure, usize), SchemaError> {
    let line = &lines[start];
    let (kind, signature) =
        if let Some(remainder) = line.trimmed.strip_prefix("mutation procedure ") {
            (ProcedureKind::Mutation, remainder)
        } else if let Some(remainder) = line.trimmed.strip_prefix("procedure ") {
            (ProcedureKind::Query, remainder)
        } else {
            return Err(SchemaError::new(
                "expected procedure declaration",
                line.start..line.start + line.raw.len(),
                line.number,
            ));
        };

    let open_paren = signature.find('(').ok_or_else(|| {
        SchemaError::new(
            "procedure declaration must include arguments parentheses",
            line.start..line.start + line.raw.len(),
            line.number,
        )
    })?;
    let close_paren = signature.rfind(')').ok_or_else(|| {
        SchemaError::new(
            "procedure declaration must close arguments parentheses",
            line.start..line.start + line.raw.len(),
            line.number,
        )
    })?;

    let name = signature[..open_paren].trim();
    let args_src = signature[open_paren + 1..close_paren].trim();
    let return_src = signature[close_paren + 1..]
        .trim()
        .strip_prefix(':')
        .map(str::trim)
        .ok_or_else(|| {
            SchemaError::new(
                "procedure declaration must include a return type",
                line.start..line.start + line.raw.len(),
                line.number,
            )
        })?;

    let mut attributes = Vec::new();
    let mut cursor = start + 1;
    while cursor < lines.len() {
        let candidate = &lines[cursor];
        if candidate.trimmed.starts_with('@') {
            attributes.push(Attribute {
                raw: candidate.trimmed.to_owned(),
                span: trimmed_span(candidate),
            });
            cursor += 1;
            continue;
        }
        if candidate.trimmed.is_empty() {
            cursor += 1;
            continue;
        }
        break;
    }

    let (procedure_docs, arg_docs) = split_procedure_docs(docs);
    let procedure_name_span = name_span_in_line(
        line,
        line.trimmed,
        if kind == ProcedureKind::Mutation {
            "mutation procedure "
        } else {
            "procedure "
        },
    )?;
    let return_type_offset = line.raw.rfind(return_src).ok_or_else(|| {
        SchemaError::new(
            "failed to locate return type in procedure declaration",
            line.start..line.start + line.raw.len(),
            line.number,
        )
    })?;

    Ok((
        Procedure {
            docs: procedure_docs,
            name: name.to_owned(),
            name_span: procedure_name_span,
            kind,
            args: parse_procedure_args(args_src, line, &arg_docs)?,
            return_type: parse_type_ref(return_src, line, return_type_offset)?,
            attributes,
            span: SourceSpan {
                start: line.start,
                end: line.start + line.raw.len(),
                line: line.number,
            },
        },
        cursor,
    ))
}

fn parse_procedure_args(
    args_src: &str,
    line: &Line<'_>,
    arg_docs: &BTreeMap<String, Vec<String>>,
) -> Result<Vec<ProcedureArg>, SchemaError> {
    if args_src.is_empty() {
        return Ok(Vec::new());
    }

    let Some(args_offset_in_line) = line.raw.find(args_src) else {
        return Err(SchemaError::new(
            "failed to locate procedure arguments in source line",
            line.start..line.start + line.raw.len(),
            line.number,
        ));
    };

    let mut args = Vec::new();
    let mut segment_start = 0usize;
    for segment in args_src.split(',') {
        let arg = segment.trim();
        if arg.is_empty() {
            segment_start += segment.len() + 1;
            continue;
        }

        let arg_offset_in_segment = segment.find(arg).unwrap_or_default();
        let arg_start = line.start + args_offset_in_line + segment_start + arg_offset_in_segment;
        let arg_end = arg_start + arg.len();
        let (name, ty) = arg.split_once(':').ok_or_else(|| {
            SchemaError::new(
                format!("invalid procedure argument: {arg}"),
                line.start..line.start + line.raw.len(),
                line.number,
            )
        })?;
        let arg_name = name.trim().to_owned();
        let name_offset_in_arg = arg.find(arg_name.as_str()).unwrap_or_default();
        let name_start = arg_start + name_offset_in_arg;
        let name_end = name_start + arg_name.len();
        let type_offset_in_arg = arg.rfind(ty.trim()).ok_or_else(|| {
            SchemaError::new(
                "failed to locate procedure argument type in source line",
                line.start..line.start + line.raw.len(),
                line.number,
            )
        })?;
        args.push(ProcedureArg {
            docs: arg_docs.get(&arg_name).cloned().unwrap_or_default(),
            name: arg_name,
            name_span: SourceSpan {
                start: name_start,
                end: name_end,
                line: line.number,
            },
            ty: parse_type_ref(ty.trim(), line, arg_start + type_offset_in_arg - line.start)?,
            span: SourceSpan {
                start: arg_start,
                end: arg_end,
                line: line.number,
            },
        });

        segment_start += segment.len() + 1;
    }

    Ok(args)
}