use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use super::items::{EnumDef, ErrorDef, FunctionDef, TypeDef};
use super::service::{HandlerContractDef, ServiceDef};
#[must_use]
pub fn cfg_feature_satisfied(cfg: Option<&str>, enabled_features: &HashSet<&str>) -> bool {
let Some(cfg_str) = cfg else {
return true;
};
if enabled_features.contains("full") {
return true;
}
if let Some(rest) = cfg_str.strip_prefix("feature = \"")
&& let Some(feature_name) = rest.strip_suffix('"')
{
return enabled_features.contains(feature_name);
}
if let Some(inner) = cfg_str
.strip_prefix("any (")
.or_else(|| cfg_str.strip_prefix("any("))
.and_then(|s| s.strip_suffix(')'))
{
let feature_names: Vec<&str> = inner
.split(',')
.filter_map(|clause| {
let trimmed = clause.trim();
trimmed.strip_prefix("feature = \"").and_then(|s| s.strip_suffix('"'))
})
.collect();
if !feature_names.is_empty() {
return feature_names.iter().any(|f| enabled_features.contains(f));
}
}
true
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ApiSurface {
pub crate_name: String,
pub version: String,
pub types: Vec<TypeDef>,
pub functions: Vec<FunctionDef>,
pub enums: Vec<EnumDef>,
pub errors: Vec<ErrorDef>,
#[serde(default)]
pub excluded_type_paths: std::collections::HashMap<String, String>,
#[serde(default)]
pub excluded_trait_names: std::collections::HashSet<String>,
#[serde(default)]
pub services: Vec<ServiceDef>,
#[serde(default)]
pub handler_contracts: Vec<HandlerContractDef>,
#[serde(default)]
pub unsupported_public_items: Vec<UnsupportedPublicItem>,
}
impl ApiSurface {
#[must_use]
pub fn with_deduped_functions(&self) -> Self {
let mut deduped = self.clone();
deduped.functions = crate::codegen::fn_dedup::dedup_same_name_functions(&self.functions);
deduped
}
#[must_use]
pub fn with_cfg_filtered(&self, enabled_features: &HashSet<&str>) -> Self {
let mut filtered = self.clone();
filtered
.types
.retain(|t| cfg_feature_satisfied(t.cfg.as_deref(), enabled_features));
filtered
.enums
.retain(|e| cfg_feature_satisfied(e.cfg.as_deref(), enabled_features));
filtered
.functions
.retain(|f| cfg_feature_satisfied(f.cfg.as_deref(), enabled_features));
filtered
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct UnsupportedPublicItem {
pub item_kind: String,
pub item_path: String,
pub reason: String,
pub suggested_fix: String,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{EnumDef, FunctionDef, TypeDef};
fn features(names: &[&'static str]) -> HashSet<&'static str> {
names.iter().copied().collect()
}
fn ty(name: &str, cfg: Option<&str>) -> TypeDef {
TypeDef {
name: name.to_string(),
cfg: cfg.map(str::to_string),
..TypeDef::default()
}
}
fn en(name: &str, cfg: Option<&str>) -> EnumDef {
EnumDef {
name: name.to_string(),
cfg: cfg.map(str::to_string),
..EnumDef::default()
}
}
fn func(name: &str, cfg: Option<&str>) -> FunctionDef {
FunctionDef {
name: name.to_string(),
cfg: cfg.map(str::to_string),
..FunctionDef::default()
}
}
#[test]
fn cfg_feature_satisfied_handles_none_and_full() {
let enabled = features(&["pdf"]);
assert!(cfg_feature_satisfied(None, &enabled));
assert!(cfg_feature_satisfied(Some("feature = \"pdf\""), &enabled));
assert!(!cfg_feature_satisfied(Some("feature = \"presets\""), &enabled));
assert!(cfg_feature_satisfied(
Some("feature = \"presets\""),
&features(&["full"])
));
}
#[test]
fn cfg_feature_satisfied_handles_any_gate() {
let enabled = features(&["ocr"]);
assert!(cfg_feature_satisfied(
Some("any(feature = \"ocr\", feature = \"paddle-ocr\")"),
&enabled
));
assert!(!cfg_feature_satisfied(
Some("any(feature = \"presets\", feature = \"heuristics\")"),
&enabled
));
}
#[test]
fn cfg_feature_satisfied_leaves_non_feature_gates_satisfied() {
assert!(cfg_feature_satisfied(
Some("target_os = \"windows\""),
&features(&["pdf"])
));
}
#[test]
fn with_cfg_filtered_drops_unsatisfied_items_only() {
let mut surface = ApiSurface::default();
surface.types.push(ty("PdfMetadata", Some("feature = \"pdf\"")));
surface.types.push(ty("Preset", Some("feature = \"presets\"")));
surface.types.push(ty("AlwaysOn", None));
surface.enums.push(en("PresetCategory", Some("feature = \"presets\"")));
surface.enums.push(en("MimeKind", None));
surface
.functions
.push(func("analyze_document", Some("feature = \"heuristics\"")));
surface.functions.push(func("extract", None));
let filtered = surface.with_cfg_filtered(&features(&["pdf"]));
let type_names: Vec<&str> = filtered.types.iter().map(|t| t.name.as_str()).collect();
assert_eq!(type_names, vec!["PdfMetadata", "AlwaysOn"]);
let enum_names: Vec<&str> = filtered.enums.iter().map(|e| e.name.as_str()).collect();
assert_eq!(enum_names, vec!["MimeKind"]);
let fn_names: Vec<&str> = filtered.functions.iter().map(|f| f.name.as_str()).collect();
assert_eq!(fn_names, vec!["extract"]);
}
#[test]
fn with_cfg_filtered_keeps_everything_under_full() {
let mut surface = ApiSurface::default();
surface.types.push(ty("Preset", Some("feature = \"presets\"")));
surface
.functions
.push(func("analyze_document", Some("feature = \"heuristics\"")));
let filtered = surface.with_cfg_filtered(&features(&["full"]));
assert_eq!(filtered.types.len(), 1);
assert_eq!(filtered.functions.len(), 1);
}
}