xidl-parser 0.72.0

A IDL codegen.
Documentation
use crate::hir;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

use super::annotations::{
    annotation_name, annotation_params, normalize_annotation_params, parse_string_array,
};

#[cfg(test)]
mod tests;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum HttpApiKeyLocation {
    Header,
    Query,
    Cookie,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum HttpSecurityRequirement {
    HttpBasic,
    HttpBearer,
    ApiKey {
        location: HttpApiKeyLocation,
        name: String,
    },
    OAuth2 {
        scopes: Vec<String>,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HttpSecurityOrigin {
    Interface,
    Method,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HttpSecurityProfile {
    pub origin: HttpSecurityOrigin,
    pub requirements: Vec<HttpSecurityRequirement>,
}

pub fn effective_security(
    interface_annotations: &[hir::Annotation],
    method_annotations: &[hir::Annotation],
) -> Result<Option<Vec<HttpSecurityRequirement>>, String> {
    let method_security = collect_security(method_annotations)?;
    if method_security.explicit_none {
        return Ok(Some(Vec::new()));
    }
    if !method_security.requirements.is_empty() {
        return Ok(Some(method_security.requirements));
    }
    let interface_security = collect_security(interface_annotations)?;
    if interface_security.explicit_none {
        return Ok(Some(Vec::new()));
    }
    if interface_security.requirements.is_empty() {
        Ok(None)
    } else {
        Ok(Some(interface_security.requirements))
    }
}

pub fn effective_security_with_origin(
    interface_annotations: &[hir::Annotation],
    method_annotations: &[hir::Annotation],
) -> Result<Option<HttpSecurityProfile>, String> {
    let method_security = collect_security(method_annotations)?;
    if method_security.explicit_none {
        return Ok(Some(HttpSecurityProfile {
            origin: HttpSecurityOrigin::Method,
            requirements: Vec::new(),
        }));
    }
    if !method_security.requirements.is_empty() {
        return Ok(Some(HttpSecurityProfile {
            origin: HttpSecurityOrigin::Method,
            requirements: method_security.requirements,
        }));
    }
    let interface_security = collect_security(interface_annotations)?;
    if interface_security.explicit_none {
        return Ok(Some(HttpSecurityProfile {
            origin: HttpSecurityOrigin::Interface,
            requirements: Vec::new(),
        }));
    }
    if interface_security.requirements.is_empty() {
        Ok(None)
    } else {
        Ok(Some(HttpSecurityProfile {
            origin: HttpSecurityOrigin::Interface,
            requirements: interface_security.requirements,
        }))
    }
}

pub(crate) struct SecurityCollection {
    pub(crate) explicit_none: bool,
    pub(crate) requirements: Vec<HttpSecurityRequirement>,
}

pub(crate) fn collect_security(
    annotations: &[hir::Annotation],
) -> Result<SecurityCollection, String> {
    let mut explicit_none = false;
    let mut requirements = Vec::new();
    let mut singleton_names = BTreeSet::new();
    for annotation in annotations {
        let Some(name) = annotation_name(annotation) else {
            continue;
        };
        if name.eq_ignore_ascii_case("no_security") {
            explicit_none = true;
            continue;
        }
        let requirement = if name.eq_ignore_ascii_case("http_basic") {
            ensure_singleton(&mut singleton_names, "http_basic")?;
            Some(HttpSecurityRequirement::HttpBasic)
        } else if name.eq_ignore_ascii_case("http_bearer") {
            ensure_singleton(&mut singleton_names, "http_bearer")?;
            Some(HttpSecurityRequirement::HttpBearer)
        } else if name.eq_ignore_ascii_case("api_key") {
            Some(parse_api_key(annotation)?)
        } else if name.eq_ignore_ascii_case("oauth2") {
            Some(parse_oauth2(annotation))
        } else {
            None
        };
        if let Some(requirement) = requirement {
            requirements.push(requirement);
        }
    }
    if explicit_none && !requirements.is_empty() {
        return Err("@no_security cannot be combined with other security annotations".to_string());
    }
    Ok(SecurityCollection {
        explicit_none,
        requirements,
    })
}

fn ensure_singleton(names: &mut BTreeSet<&'static str>, value: &'static str) -> Result<(), String> {
    if !names.insert(value) {
        return Err(format!("duplicate @{value} annotation"));
    }
    Ok(())
}

fn parse_api_key(annotation: &hir::Annotation) -> Result<HttpSecurityRequirement, String> {
    let params = annotation_params(annotation)
        .ok_or_else(|| "@api_key requires in=... and name=...".to_string())?;
    let params = normalize_annotation_params(params);
    let location = match params.get("in").map(|value| value.to_ascii_lowercase()) {
        Some(value) if value == "header" => HttpApiKeyLocation::Header,
        Some(value) if value == "query" => HttpApiKeyLocation::Query,
        Some(value) if value == "cookie" => HttpApiKeyLocation::Cookie,
        Some(value) => {
            return Err(format!(
                "@api_key(in=...) must be one of header|query|cookie, got '{value}'"
            ));
        }
        None => return Err("@api_key requires non-empty in=...".to_string()),
    };
    let name = params
        .get("name")
        .cloned()
        .filter(|value| !value.is_empty())
        .ok_or_else(|| "@api_key requires non-empty name=...".to_string())?;
    Ok(HttpSecurityRequirement::ApiKey { location, name })
}

fn parse_oauth2(annotation: &hir::Annotation) -> HttpSecurityRequirement {
    let params = annotation_params(annotation)
        .map(normalize_annotation_params)
        .unwrap_or_default();
    let scopes = params
        .get("scopes")
        .map(|value| parse_string_array(value))
        .unwrap_or_default();
    HttpSecurityRequirement::OAuth2 { scopes }
}