xidl-parser 0.105.0

A IDL codegen.
Documentation
use super::HttpParamKind;
use super::model::{HttpException, HttpExceptionField, HttpExceptionMember, HttpExceptionRef};
use super::semantics::{SourceBinding, normalize_annotation_params};
use crate::error::{ParseError, ParserResult};
use crate::hir;
use std::collections::HashSet;

impl HttpException {
    pub(super) fn from_hir(except: &hir::ExceptDcl, module_path: &[String]) -> ParserResult<Self> {
        let status = Self::status(&except.annotations, &except.ident)?;
        let mut headers = Vec::new();
        let mut cookies = Vec::new();
        let mut body = Vec::new();
        for member in &except.member {
            for decl in &member.ident {
                let field = match decl {
                    hir::Declarator::SimpleDeclarator(value) => value.0.clone(),
                    hir::Declarator::ArrayDeclarator(value) => {
                        return Err(ParseError::Message(format!(
                            "exception '{}': array declarators are not supported ('{}')",
                            except.ident, value.ident
                        )));
                    }
                };
                let owner = format!("{}::{field}", except.ident);
                let binding = SourceBinding::read(&member.annotations, &owner)
                    .map_err(ParseError::Message)?;
                if binding.as_ref().is_some_and(|binding| {
                    matches!(binding.source, HttpParamKind::Path | HttpParamKind::Query)
                }) {
                    return Err(ParseError::Message(format!(
                        "HTTP exception field '{owner}' cannot use request path or query bindings"
                    )));
                }
                // JSON error fields keep the data model's rename rules. Source
                // names describe headers and cookies, outside that JSON body.
                let wire_name = binding
                    .as_ref()
                    .filter(|binding| binding.source != HttpParamKind::Body)
                    .map_or_else(
                        || {
                            hir::effective_wire_name(
                                &field,
                                &member.annotations,
                                &except.annotations,
                            )
                        },
                        |binding| {
                            binding.wire_name(&field, &member.annotations, &except.annotations)
                        },
                    );
                if binding
                    .as_ref()
                    .is_some_and(|binding| binding.source == HttpParamKind::Header)
                {
                    super::validate::validate_header_name(&wire_name, &owner)
                        .map_err(ParseError::Message)?;
                    headers.push(HttpExceptionMember {
                        field,
                        wire_name,
                        ty: member.ty.clone(),
                        is_multi: matches!(member.ty, hir::TypeSpec::SequenceType(_)),
                        optional: member.is_optional(),
                    });
                } else if binding
                    .as_ref()
                    .is_some_and(|binding| binding.source == HttpParamKind::Cookie)
                {
                    super::validate::validate_cookie_name(&wire_name, &owner)
                        .map_err(ParseError::Message)?;
                    cookies.push(HttpExceptionMember {
                        field,
                        wire_name,
                        ty: member.ty.clone(),
                        is_multi: matches!(member.ty, hir::TypeSpec::SequenceType(_)),
                        optional: member.is_optional(),
                    });
                } else {
                    body.push(HttpExceptionField {
                        field,
                        wire_name,
                        optional: member.is_optional(),
                        ty: member.ty.clone(),
                    });
                }
            }
        }
        if status == 304 && !body.is_empty() {
            return Err(ParseError::Message(format!(
                "exception '{}': HTTP 304 cannot carry body members",
                except.ident
            )));
        }
        Ok(Self {
            module_path: module_path.to_vec(),
            ident: except.ident.clone(),
            status,
            headers,
            cookies,
            body,
        })
    }

    fn http_params(annotations: &[hir::Annotation]) -> Option<&hir::AnnotationParams> {
        annotations.iter().find_map(|annotation| {
            (hir::annotation_name(annotation).and_then(crate::lint::AnnotationKind::from_name)
                == Some(crate::lint::AnnotationKind::Http))
            .then(|| hir::annotation_params(annotation))
            .flatten()
        })
    }

    pub(crate) fn status(annotations: &[hir::Annotation], ident: &str) -> ParserResult<u16> {
        const HINT: &str = "(e.g. @http(412) or @http(status = 412))";
        let params = Self::http_params(annotations).ok_or_else(|| {
            ParseError::Message(format!(
                "exception '{ident}': @http <status> is required for the HTTP error channel {HINT}"
            ))
        })?;
        let params = normalize_annotation_params(params);
        let raw = params
            .get("status")
            .or_else(|| params.get("value"))
            .ok_or_else(|| {
                ParseError::Message(format!(
                    "exception '{ident}': @http requires an integer status {HINT}"
                ))
            })?;
        let status: u16 = raw.trim().parse().map_err(|_| {
            ParseError::Message(format!(
                "exception '{ident}': @http status '{raw}' is not an integer {HINT}"
            ))
        })?;
        if !(300..=599).contains(&status) {
            return Err(ParseError::Message(format!(
                "exception '{ident}': @http status {status} must be in 300..=599; success responses belong to the return type"
            )));
        }
        Ok(status)
    }

    fn resolve<'a>(
        exceptions: &'a [HttpException],
        module_path: &[String],
        scoped: &hir::ScopedName,
    ) -> Option<&'a HttpException> {
        let ident = scoped.name.last()?;
        let prefix = &scoped.name[..scoped.name.len() - 1];
        if scoped.is_root {
            return exceptions
                .iter()
                .find(|e| &e.ident == ident && e.module_path.as_slice() == prefix);
        }
        let depth = module_path.len();
        for skip in (0..=depth).rev() {
            let scope = &module_path[..skip];
            if let Some(found) = exceptions
                .iter()
                .find(|e| &e.ident == ident && e.module_path.iter().eq(scope.iter().chain(prefix)))
            {
                return Some(found);
            }
        }
        None
    }
}

impl HttpExceptionRef {
    pub(super) fn for_operation(
        op: &hir::OpDcl,
        module_path: &[String],
        exceptions: &[HttpException],
    ) -> ParserResult<Vec<HttpExceptionRef>> {
        let Some(expr) = &op.raises else {
            return Ok(Vec::new());
        };
        let op_ident = &op.ident;
        let mut refs = Vec::new();
        let mut statuses = HashSet::new();
        for scoped in &expr.0 {
            let exception = HttpException::resolve(exceptions, module_path, scoped).ok_or_else(|| {
            ParseError::Message(format!(
                "operation '{op_ident}': raises('{}') does not resolve to a declared exception",
                scoped.name.join("::")
            ))
        })?;
            if !statuses.insert(exception.status) {
                return Err(ParseError::Message(format!(
                    "operation '{op_ident}': duplicate HTTP status {} in raises(...)",
                    exception.status
                )));
            }
            refs.push(HttpExceptionRef {
                module_path: exception.module_path.clone(),
                ident: exception.ident.clone(),
            });
        }
        Ok(refs)
    }
}