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, Field, MixinDecl, Model};

use crate::diagnostics::SchemaError;
use crate::line_helpers::{Line, parse_doc_comment, trimmed_span};
use crate::parse::fields::parse_field;

pub(super) fn parse_model_body(
    lines: &[Line<'_>],
) -> Result<(Vec<Field>, Vec<Attribute>), SchemaError> {
    let mut fields = Vec::new();
    let mut attributes = Vec::new();
    let mut pending_docs = Vec::new();
    for line in lines {
        if let Some(doc) = parse_doc_comment(line) {
            pending_docs.push(doc.to_owned());
            continue;
        }
        if line.trimmed.is_empty() {
            pending_docs.clear();
            continue;
        }
        if line.trimmed.starts_with("//") {
            pending_docs.clear();
            continue;
        }
        if line.trimmed.starts_with("@@") || line.trimmed.starts_with("@use(") {
            pending_docs.clear();
            attributes.push(Attribute {
                raw: line.trimmed.to_owned(),
                span: trimmed_span(line),
            });
            continue;
        }
        if line.trimmed.starts_with('@') {
            return Err(SchemaError::new(
                format!("unsupported model directive `{}`", line.trimmed),
                line.start..line.start + line.raw.len(),
                line.number,
            ));
        }
        fields.push(parse_field(line, std::mem::take(&mut pending_docs))?);
    }
    Ok((fields, attributes))
}

pub(super) fn expand_model_mixins(
    mixins: &[MixinDecl],
    models: &mut [Model],
) -> Result<(), SchemaError> {
    let mixins_by_name = mixins
        .iter()
        .map(|mixin| (mixin.name.clone(), mixin))
        .collect::<BTreeMap<_, _>>();

    for model in models.iter_mut() {
        let mut expanded_fields = Vec::new();
        let mut field_names = model
            .fields
            .iter()
            .map(|field| field.name.clone())
            .collect::<std::collections::BTreeSet<_>>();
        let mut retained_attributes = Vec::new();

        for attribute in &model.attributes {
            if !attribute.raw.starts_with("@use(") {
                retained_attributes.push(attribute.clone());
                continue;
            }

            let mixin_names = parse_model_use_attribute(attribute)?;
            for mixin_name in mixin_names {
                let mixin = mixins_by_name.get(&mixin_name).ok_or_else(|| {
                    SchemaError::new(
                        format!(
                            "model `{}` references unknown mixin `{}` in {}",
                            model.name, mixin_name, attribute.raw
                        ),
                        attribute.span.start..attribute.span.end,
                        attribute.span.line,
                    )
                })?;

                for field in &mixin.fields {
                    if field_names.insert(field.name.clone()) {
                        expanded_fields.push(field.clone());
                    }
                }
            }
        }

        expanded_fields.extend(model.fields.clone());
        model.fields = expanded_fields;
        model.attributes = retained_attributes;
    }

    Ok(())
}

fn parse_model_use_attribute(attribute: &Attribute) -> Result<Vec<String>, SchemaError> {
    let Some(inner) = attribute
        .raw
        .strip_prefix("@use(")
        .and_then(|value| value.strip_suffix(')'))
    else {
        return Err(SchemaError::new(
            format!("invalid model use attribute `{}`", attribute.raw),
            attribute.span.start..attribute.span.end,
            attribute.span.line,
        ));
    };
    let names = inner
        .split(',')
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
        .collect::<Vec<_>>();
    if names.is_empty() {
        return Err(SchemaError::new(
            format!(
                "model use attribute `{}` must list at least one mixin",
                attribute.raw
            ),
            attribute.span.start..attribute.span.end,
            attribute.span.line,
        ));
    }
    Ok(names)
}