xidl-parser 0.72.0

A IDL codegen.
Documentation
use crate::error::{ParseError, ParserResult};
use crate::hir;
use std::collections::HashSet;

use super::attr::project_attribute;
use super::mapping;
use super::project_params::project_params;
use super::route::{
    auto_default_method_path, operation_id, parse_route_template, route_from_annotations,
};
use super::semantics::{
    HttpStreamKind, effective_cors, effective_media_type, effective_security_with_origin,
    has_annotation, http_stream_config, validate_http_annotations,
};
use super::validate::{
    effective_basic_auth_realm, effective_deprecated, validate_head_constraints,
    validate_request_shape, validate_route_bindings, validate_stream_method, validate_stream_shape,
    validate_upgrade_constraints,
};
use super::{
    HttpDocumentMetadata, HttpDocumentServer, HttpInterface, HttpOperation, HttpOperationMeta,
    HttpOperationSource, RestHirDocument,
};

pub fn project(spec: &hir::Specification) -> ParserResult<RestHirDocument> {
    let mut ctx = ProjectionContext::default();
    ctx.collect_spec(spec, &[])?;
    Ok(RestHirDocument {
        spec: spec.clone(),
        document: ctx.document,
        interfaces: ctx.interfaces,
    })
}

fn parse_err(message: String) -> ParseError {
    ParseError::Message(message)
}

#[derive(Default)]
struct ProjectionContext {
    document: HttpDocumentMetadata,
    interfaces: Vec<HttpInterface>,
}

impl ProjectionContext {
    fn collect_spec(
        &mut self,
        spec: &hir::Specification,
        module_path: &[String],
    ) -> ParserResult<()> {
        for def in &spec.0 {
            self.collect_def(def, module_path)?;
        }
        Ok(())
    }

    fn collect_def(&mut self, def: &hir::Definition, module_path: &[String]) -> ParserResult<()> {
        match def {
            hir::Definition::ModuleDcl(module) => {
                let mut next = module_path.to_vec();
                next.push(module.ident.clone());
                for def in &module.definition {
                    self.collect_def(def, &next)?;
                }
            }
            hir::Definition::Pragma(pragma) => self.apply_pragma(pragma),
            hir::Definition::InterfaceDcl(interface) => {
                if let Some(interface) = project_interface(interface, module_path)? {
                    self.interfaces.push(interface);
                }
            }
            _ => {}
        }
        Ok(())
    }

    fn apply_pragma(&mut self, pragma: &hir::Pragma) {
        match pragma {
            hir::Pragma::XidlcPackage(value) if !value.is_empty() => {
                self.document.package = Some(normalize_pragma_scalar(value));
            }
            hir::Pragma::XidlcOpenApiVersion(value) if !value.is_empty() => {
                self.document.version = Some(normalize_pragma_scalar(value));
            }
            hir::Pragma::XidlcOpenApiService {
                base_url,
                description,
            } if !base_url.is_empty() => {
                self.document.servers.push(HttpDocumentServer {
                    base_url: base_url.clone(),
                    description: description.clone(),
                });
            }
            _ => {}
        }
    }
}

fn normalize_pragma_scalar(value: &str) -> String {
    value
        .trim()
        .trim_start_matches('=')
        .trim()
        .trim_matches('"')
        .trim_matches('\'')
        .to_string()
}

fn project_interface(
    interface: &hir::InterfaceDcl,
    module_path: &[String],
) -> ParserResult<Option<HttpInterface>> {
    let hir::InterfaceDclInner::InterfaceDef(def) = &interface.decl else {
        return Ok(None);
    };
    validate_http_annotations(
        &format!("interface '{}'", def.header.ident),
        &interface.annotations,
    )
    .map_err(ParseError::Message)?;

    let mut operations = Vec::new();
    if let Some(body) = &def.interface_body {
        for export in &body.0 {
            match export {
                hir::Export::OpDcl(op) => operations.push(project_operation(
                    &def.header.ident,
                    module_path,
                    &interface.annotations,
                    op,
                )?),
                hir::Export::AttrDcl(attr) => operations.extend(project_attribute_group(
                    &def.header.ident,
                    module_path,
                    &interface.annotations,
                    attr,
                )?),
                _ => {}
            }
        }
    }

    Ok(Some(HttpInterface {
        name: def.header.ident.clone(),
        module_path: module_path.to_vec(),
        operations,
    }))
}

fn project_attribute_group(
    interface_name: &str,
    module_path: &[String],
    interface_annotations: &[hir::Annotation],
    attr: &hir::AttrDcl,
) -> ParserResult<Vec<HttpOperation>> {
    validate_http_annotations(
        &format!("attribute in interface '{interface_name}'"),
        &attr.annotations,
    )
    .map_err(ParseError::Message)?;
    if has_annotation(&attr.annotations, "cors") {
        return Err(ParseError::Message(format!(
            "attribute in interface '{interface_name}': @cors is only supported on interfaces and methods"
        )));
    }
    let cors = effective_cors(interface_annotations, &[]).map_err(ParseError::Message)?;
    let deprecated = effective_deprecated(interface_annotations, &attr.annotations)
        .map_err(ParseError::Message)?;
    let security = effective_security_with_origin(interface_annotations, &attr.annotations)
        .map_err(ParseError::Message)?;
    let mut operations = project_attribute(
        interface_name,
        module_path,
        attr,
        security,
        deprecated,
        has_annotation(&attr.annotations, "server_stream"),
    );
    for operation in &mut operations {
        operation.meta.cors = cors.clone();
    }
    Ok(operations)
}

fn project_operation(
    interface_name: &str,
    module_path: &[String],
    interface_annotations: &[hir::Annotation],
    op: &hir::OpDcl,
) -> ParserResult<HttpOperation> {
    validate_http_annotations(&format!("operation '{}'", op.ident), &op.annotations)
        .map_err(ParseError::Message)?;
    let stream = http_stream_config(&op.annotations).map_err(ParseError::Message)?;
    validate_stream_shape(&op.ident, stream).map_err(parse_err)?;
    let has_upgrade = has_annotation(&op.annotations, "upgrade");
    let default_method = if matches!(
        stream.kind,
        Some(HttpStreamKind::Server) | Some(HttpStreamKind::Bidi)
    ) || has_upgrade
    {
        super::HttpMethod::Get
    } else {
        super::HttpMethod::Post
    };
    let (method, mut route_paths) =
        route_from_annotations(&op.annotations, default_method).map_err(parse_err)?;
    validate_stream_method(&op.ident, stream.kind, method).map_err(parse_err)?;
    if route_paths.is_empty() {
        route_paths.push(auto_default_method_path(op, method).map_err(parse_err)?);
    }
    let routes = route_paths
        .iter()
        .map(|path| parse_route_template(path).map_err(parse_err))
        .collect::<ParserResult<Vec<_>>>()?;
    let route_path_names = routes
        .iter()
        .map(|route| route.path_params.iter().cloned().collect::<HashSet<_>>())
        .collect::<Vec<_>>();
    let route_query_names = routes
        .iter()
        .map(|route| route.query_params.iter().cloned().collect::<HashSet<_>>())
        .collect::<Vec<_>>();

    let (request_params, response_params, path_binding_count, query_binding_count) =
        project_params(
            op,
            method,
            stream.kind,
            &route_path_names,
            &route_query_names,
        )
        .map_err(parse_err)?;

    validate_route_bindings(
        &op.ident,
        &routes,
        &path_binding_count,
        &query_binding_count,
    )
    .map_err(parse_err)?;
    validate_request_shape(&op.ident, stream.kind, &request_params).map_err(parse_err)?;
    validate_head_constraints(
        &op.ident,
        method,
        &response_params,
        match &op.ty {
            hir::OpTypeSpec::Void => None,
            hir::OpTypeSpec::TypeSpec(ty) => Some(ty),
        },
    )
    .map_err(parse_err)?;

    let upgrade_protocol = super::semantics::find_annotation(&op.annotations, "upgrade")
        .and_then(super::semantics::annotation_params)
        .map(super::semantics::normalize_annotation_params)
        .and_then(|params| params.get("protocol").cloned());

    validate_upgrade_constraints(
        &op.ident,
        has_upgrade,
        upgrade_protocol.as_deref(),
        method,
        &request_params,
        match &op.ty {
            hir::OpTypeSpec::Void => None,
            hir::OpTypeSpec::TypeSpec(ty) => Some(ty),
        },
    )
    .map_err(parse_err)?;

    let request_content_type =
        effective_media_type(interface_annotations, &op.annotations, "Consumes");
    let response_content_type =
        effective_media_type(interface_annotations, &op.annotations, "Produces");
    let cors =
        effective_cors(interface_annotations, &op.annotations).map_err(ParseError::Message)?;
    let security = effective_security_with_origin(interface_annotations, &op.annotations)
        .map_err(ParseError::Message)?;
    let basic_auth_realm = effective_basic_auth_realm(interface_annotations, &op.annotations);
    let deprecated = effective_deprecated(interface_annotations, &op.annotations)
        .map_err(ParseError::Message)?;

    let return_type = match &op.ty {
        hir::OpTypeSpec::Void => None,
        hir::OpTypeSpec::TypeSpec(ty) => Some(ty.clone()),
    };

    let signature = mapping::build_operation_signature(op);
    let http = mapping::build_http_mapping(
        method,
        &stream,
        &request_content_type,
        &response_content_type,
        &request_params,
        &response_params,
        &return_type,
    );

    Ok(HttpOperation {
        meta: HttpOperationMeta {
            name: op.ident.clone(),
            operation_id: operation_id(module_path, interface_name, &op.ident),
            source: HttpOperationSource::Method,
            method,
            routes,
            stream,
            cors,
            security,
            basic_auth_realm,
            deprecated,
            upgrade_protocol,
        },
        signature,
        http,
    })
}