xidl-parser 0.72.0

A IDL codegen.
Documentation
use std::collections::HashSet;

use crate::error::{ParseError, ParserResult};
use crate::hir;

use super::JsonRpcMethodKind;

#[cfg(test)]
mod tests;

pub(super) fn param_is_input(attr: Option<&hir::ParamAttribute>) -> bool {
    !matches!(attr.map(|value| value.0.as_str()), Some("out"))
}

pub(super) fn param_is_output(attr: Option<&hir::ParamAttribute>) -> bool {
    matches!(attr.map(|value| value.0.as_str()), Some("out" | "inout"))
}

pub(super) fn stream_kind(
    annotations: &[hir::Annotation],
) -> ParserResult<Option<JsonRpcMethodKind>> {
    let mut out = None;
    for annotation in annotations {
        let Some(name) = annotation_name(annotation) else {
            continue;
        };
        let current = if name.eq_ignore_ascii_case("server_stream") {
            Some(JsonRpcMethodKind::ServerStream)
        } else if name.eq_ignore_ascii_case("client_stream") {
            Some(JsonRpcMethodKind::ClientStream)
        } else if name.eq_ignore_ascii_case("bidi_stream") {
            Some(JsonRpcMethodKind::BidiStream)
        } else {
            None
        };
        let Some(current) = current else {
            continue;
        };
        match out {
            None => out = Some(current),
            Some(prev) if prev == current => {}
            Some(_) => {
                return Err(ParseError::Message(
                    "@server_stream/@client_stream/@bidi_stream are mutually exclusive".to_string(),
                ));
            }
        }
    }
    Ok(out)
}

pub(super) fn has_annotation(annotations: &[hir::Annotation], target: &str) -> bool {
    annotations.iter().any(|annotation| {
        annotation_name(annotation)
            .map(|name| name.eq_ignore_ascii_case(target))
            .unwrap_or(false)
    })
}

pub(super) fn has_optional_annotation(annotations: &[hir::Annotation]) -> bool {
    annotations.iter().any(|annotation| {
        matches!(annotation, hir::Annotation::Optional { .. })
            || annotation_name(annotation)
                .map(|name| name.eq_ignore_ascii_case("optional"))
                .unwrap_or(false)
    })
}

pub(super) fn validate_attr_collision(
    user_ops: &HashSet<&str>,
    attr_name: &str,
    getter: &str,
    setter: &str,
) -> ParserResult<()> {
    let getter_conflict = user_ops.contains(getter);
    let setter_conflict = !setter.is_empty() && user_ops.contains(setter);
    if getter_conflict || setter_conflict {
        let conflict = if setter.is_empty() {
            format!("`{getter}`")
        } else {
            format!("`{getter}` or `{setter}`")
        };
        return Err(ParseError::Message(format!(
            "attribute `{attr_name}` conflicts with user-defined operation `{conflict}`"
        )));
    }
    Ok(())
}

fn annotation_name(annotation: &hir::Annotation) -> Option<&str> {
    match annotation {
        hir::Annotation::Builtin { name, .. } => Some(name.as_str()),
        hir::Annotation::ScopedName { name, .. } => name.name.last().map(|value| value.as_str()),
        _ => None,
    }
}