xidl-parser 0.105.0

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

use crate::{hir, typed_ast as ast};

use super::{AnnotationDefinition, AnnotationKind, ArgumentShape, Site};

struct Arguments {
    values: BTreeMap<String, String>,
    positional: usize,
    named: usize,
    strings_only: bool,
}

impl Arguments {
    fn read(annotation: &ast::AnnotationAppl) -> Result<Self, String> {
        let mut result = Self {
            values: BTreeMap::new(),
            positional: 0,
            named: 0,
            strings_only: true,
        };
        match &annotation.params {
            None => {}
            Some(ast::AnnotationParams::Params(params)) => {
                for param in params {
                    let (key, expression) = match param {
                        ast::AnnotationApplParam::Positional(expr) => {
                            result.positional += 1;
                            ("value".to_string(), expr)
                        }
                        ast::AnnotationApplParam::Named { ident, value } => {
                            result.named += 1;
                            (ident.0.to_ascii_lowercase(), value)
                        }
                    };
                    let normalized = hir::normalize_annotation_params(
                        &hir::AnnotationParams::ConstExpr(expression.clone().into()),
                    );
                    result.strings_only &= matches!(
                        hir::ConstExpr::from(expression.clone()),
                        hir::ConstExpr::Literal(hir::Literal::StringLiteral(_))
                    );
                    let value = normalized.get("value").cloned().unwrap_or_default();
                    if result.values.insert(key.clone(), value).is_some()
                        && (key != "value" || result.named > 0)
                    {
                        return Err(format!("duplicate annotation argument `{key}`"));
                    }
                }
                if result.named > 0 && result.positional > 0 {
                    return Err("cannot mix named and positional annotation arguments".to_string());
                }
            }
            Some(ast::AnnotationParams::Raw(raw)) => {
                let raw = crate::parser::raw::RawArguments::without_comments(raw);
                for part in crate::parser::raw::RawArguments::split(&raw, ',') {
                    let pair = crate::parser::raw::RawArguments::split(part, '=');
                    let (key, value) = match pair.as_slice() {
                        [value] => {
                            result.positional += 1;
                            ("value", *value)
                        }
                        [key, value] => {
                            result.named += 1;
                            (*key, *value)
                        }
                        _ => return Err("invalid annotation argument".to_string()),
                    };
                    result.strings_only &=
                        value.starts_with('"') && value.ends_with('"') && value.len() >= 2;
                    let normalized = hir::normalize_annotation_params(&hir::AnnotationParams::Raw(
                        value.to_string(),
                    ));
                    let value = normalized
                        .get("value")
                        .cloned()
                        .unwrap_or_else(|| value.to_string());
                    let key = key.to_ascii_lowercase();
                    if result.values.insert(key.clone(), value).is_some()
                        && (key != "value" || result.named > 0)
                    {
                        return Err(format!("duplicate annotation argument `{key}`"));
                    }
                }
            }
        }
        if result.named > 0 && result.positional > 0 {
            return Err("cannot mix named and positional annotation arguments".to_string());
        }
        Ok(result)
    }

    fn keys(&self, required: &[&str], optional: &[&str]) -> Result<(), String> {
        for key in required {
            if !self.values.contains_key(*key) {
                return Err(format!("requires `{key}`"));
            }
        }
        for key in self.values.keys() {
            if !required.contains(&key.as_str()) && !optional.contains(&key.as_str()) {
                return Err(format!("unknown argument `{key}`"));
            }
        }
        Ok(())
    }

    fn single(&self, aliases: &[&str], optional: bool) -> Result<(), String> {
        if optional && self.values.is_empty() {
            return Ok(());
        }
        if self.values.len() != 1 || self.positional > 1 {
            return Err("requires exactly one value".to_string());
        }
        if self
            .values
            .keys()
            .all(|key| key == "value" || aliases.contains(&key.as_str()))
        {
            Ok(())
        } else {
            Err(format!(
                "expected a value or one of: {}",
                aliases.join(", ")
            ))
        }
    }

    fn location(&self) -> Result<(), String> {
        self.keys(&["in", "name"], &[])?;
        let location = self.values.get("in").map(String::as_str).unwrap_or("");
        if !["header", "query", "cookie"]
            .iter()
            .any(|name| name.eq_ignore_ascii_case(location))
        {
            return Err("`in` must be header, query or cookie".to_string());
        }
        if self.values.get("name").is_none_or(|name| name.is_empty()) {
            return Err("requires a non-empty `name`".to_string());
        }
        Ok(())
    }

    fn strings(&self) -> Result<(), String> {
        if self.strings_only {
            Ok(())
        } else {
            Err("requires string literal arguments".to_string())
        }
    }
}

impl AnnotationDefinition {
    pub(super) fn check_arguments(
        &self,
        annotation: &ast::AnnotationAppl,
        site: Site,
    ) -> Result<(), String> {
        let args = Arguments::read(annotation)?;
        match self.arguments {
            ArgumentShape::Marker => args.keys(&[], &[]),
            ArgumentShape::Value => args.single(&[], false),
            ArgumentShape::OptionalValue => args.single(&[], true),
            ArgumentShape::String(aliases) => {
                args.single(aliases, false)?;
                args.strings()
            }
            ArgumentShape::OptionalString(aliases) => {
                args.single(aliases, true)?;
                args.strings()
            }
            ArgumentShape::Named { required, optional } => args.keys(required, optional),
            ArgumentShape::Version if annotation.params.is_none() => Ok(()),
            ArgumentShape::Version | ArgumentShape::ApiKey => {
                args.location()?;
                args.strings()
            }
            ArgumentShape::Http if site == Site::Exception => {
                args.single(&["status"], false)?;
                crate::rest_hir::HttpException::status(&[annotation.clone().into()], "annotation")
                    .map(|_| ())
                    .map_err(|error| error.to_string())
            }
            ArgumentShape::Http => args.keys(&[], &[]),
            ArgumentShape::Values if annotation.params.is_some() => Ok(()),
            ArgumentShape::Values => Err("requires at least one value".to_string()),
            ArgumentShape::Cors => {
                if args.named > 0 {
                    return Err("requires positional origin strings".to_string());
                }
                args.strings()?;
                crate::rest_hir::semantics::effective_cors(&[], &[annotation.clone().into()])
                    .map(|_| ())
            }
            ArgumentShape::Deprecated => {
                if args.positional > 0 {
                    args.single(&[], false)?;
                } else {
                    args.keys(&[], &["since", "after"])?;
                }
                crate::rest_hir::semantics::deprecated_info(&[annotation.clone().into()])
                    .map(|_| ())
            }
            ArgumentShape::Lifecycle => match &annotation.params {
                Some(ast::AnnotationParams::Params(params))
                    if matches!(params.as_slice(),
                    [ast::AnnotationApplParam::Positional(expr)] if matches!(hir::ConstExpr::from(expr.clone()), hir::ConstExpr::ScopedName(_))) =>
                {
                    Ok(())
                }
                _ => Err(
                    "requires one scoped version reference, for example `Versions::v2`".to_string(),
                ),
            },
            ArgumentShape::Grammar if annotation.builtin.is_some() => Ok(()),
            ArgumentShape::Grammar => {
                Err("arguments do not match the built-in IDL grammar".to_string())
            }
        }?;
        if self.kind == AnnotationKind::Upgrade {
            let annotations = [annotation.clone().into()];
            let protocol = crate::rest_hir::semantics::parse_upgrade_protocol(&annotations)?
                .ok_or_else(|| "requires a protocol".to_string())?;
            let mode = crate::rest_hir::semantics::classify_upgrade_protocol(&protocol)?;
            crate::rest_hir::semantics::parse_websocket_config(&annotations, mode)?;
        }
        Ok(())
    }
}