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;
}
cfg_expr_satisfied(cfg_str, enabled_features).unwrap_or(true)
}
fn cfg_expr_satisfied(cfg_str: &str, enabled_features: &HashSet<&str>) -> Option<bool> {
let normalized = cfg_str.trim().replace(" (", "(");
let cfg_str = normalized.as_str();
if let Some(rest) = cfg_str.strip_prefix("feature = \"")
&& let Some(feature_name) = rest.strip_suffix('"')
{
return Some(enabled_features.contains(feature_name));
}
if let Some(inner) = cfg_str.strip_prefix("any(").and_then(|s| s.strip_suffix(')')) {
let mut saw_indeterminate = false;
for cond in split_cfg_operands(inner) {
match cfg_expr_satisfied(&cond, enabled_features) {
Some(true) => return Some(true),
Some(false) => {}
None => saw_indeterminate = true,
}
}
return if saw_indeterminate { None } else { Some(false) };
}
if let Some(inner) = cfg_str.strip_prefix("all(").and_then(|s| s.strip_suffix(')')) {
let mut saw_indeterminate = false;
for cond in split_cfg_operands(inner) {
match cfg_expr_satisfied(&cond, enabled_features) {
Some(false) => return Some(false),
Some(true) => {}
None => saw_indeterminate = true,
}
}
return if saw_indeterminate { None } else { Some(true) };
}
if let Some(inner) = cfg_str.strip_prefix("not(").and_then(|s| s.strip_suffix(')')) {
return cfg_expr_satisfied(inner.trim(), enabled_features).map(|value| !value);
}
None
}
fn split_cfg_operands(inner: &str) -> Vec<String> {
let mut result = Vec::new();
let mut depth = 0usize;
let mut current = String::new();
for ch in inner.chars() {
match ch {
'(' => {
depth += 1;
current.push(ch);
}
')' => {
depth = depth.saturating_sub(1);
current.push(ch);
}
',' if depth == 0 => {
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
result.push(trimmed);
}
current.clear();
}
_ => current.push(ch),
}
}
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
result.push(trimmed);
}
result
}
#[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
}
#[must_use]
pub fn with_cfg_filtered_deep(&self, enabled_features: &HashSet<&str>) -> Self {
let mut filtered = self.with_cfg_filtered(enabled_features);
for typ in &mut filtered.types {
typ.fields
.retain(|field| cfg_feature_satisfied(field.cfg.as_deref(), enabled_features));
}
for enum_def in &mut filtered.enums {
enum_def
.variants
.retain(|variant| cfg_feature_satisfied(variant.cfg.as_deref(), enabled_features));
for variant in &mut enum_def.variants {
variant
.fields
.retain(|field| cfg_feature_satisfied(field.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_handles_all_and_not_gates() {
let enabled = features(&["layout-types"]);
assert!(cfg_feature_satisfied(
Some("all(feature = \"layout-types\", not(feature = \"wasm-target\"))"),
&enabled
));
assert!(!cfg_feature_satisfied(
Some("not(feature = \"layout-types\")"),
&enabled
));
assert!(cfg_feature_satisfied(Some("not(feature = \"wasm-target\")"), &enabled));
assert!(!cfg_feature_satisfied(
Some("all(feature = \"layout-types\", feature = \"wasm-target\")"),
&enabled
));
}
#[test]
fn cfg_feature_satisfied_handles_whitespace_and_nested_operands() {
let enabled = features(&["wasm-target"]);
assert!(cfg_feature_satisfied(
Some("any (feature = \"wasm-target\" , all(feature = \"a\", feature = \"b\"))"),
&enabled
));
}
#[test]
fn cfg_feature_satisfied_leaves_non_feature_gates_satisfied() {
assert!(cfg_feature_satisfied(
Some("target_os = \"windows\""),
&features(&["pdf"])
));
}
#[test]
fn cfg_feature_satisfied_keeps_items_gated_on_target_predicates() {
let enabled = features(&["liter-llm"]);
assert!(cfg_feature_satisfied(
Some("all (feature = \"liter-llm\" , not (target_arch = \"wasm32\"))"),
&enabled
));
assert!(!cfg_feature_satisfied(
Some("all (feature = \"liter-llm\" , not (target_arch = \"wasm32\"))"),
&features(&["pdf"])
));
assert!(cfg_feature_satisfied(
Some("not (target_arch = \"wasm32\")"),
&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_deep_drops_gated_fields_and_variants() {
use crate::core::ir::{EnumVariant, FieldDef};
let mut config_ty = ty("ExtractionConfig", None);
config_ty.fields = vec![
FieldDef {
name: "use_cache".to_string(),
cfg: None,
..FieldDef::default()
},
FieldDef {
name: "tree_sitter".to_string(),
cfg: Some("feature = \"tree-sitter\"".to_string()),
..FieldDef::default()
},
];
let mut fmt_enum = en("OutputFormat", None);
fmt_enum.variants = vec![
EnumVariant {
name: "Markdown".to_string(),
cfg: None,
..EnumVariant::default()
},
EnumVariant {
name: "CodeAst".to_string(),
cfg: Some("feature = \"tree-sitter\"".to_string()),
..EnumVariant::default()
},
];
let mut surface = ApiSurface::default();
surface.types.push(config_ty);
surface
.types
.push(ty("TreeSitterConfig", Some("feature = \"tree-sitter\"")));
surface.enums.push(fmt_enum);
let filtered = surface.with_cfg_filtered_deep(&features(&["pdf"]));
let type_names: Vec<&str> = filtered.types.iter().map(|t| t.name.as_str()).collect();
assert_eq!(type_names, vec!["ExtractionConfig"]);
let field_names: Vec<&str> = filtered.types[0].fields.iter().map(|f| f.name.as_str()).collect();
assert_eq!(field_names, vec!["use_cache"]);
let variant_names: Vec<&str> = filtered.enums[0].variants.iter().map(|v| v.name.as_str()).collect();
assert_eq!(variant_names, vec!["Markdown"]);
}
#[test]
fn with_cfg_filtered_deep_keeps_gated_members_under_full() {
use crate::core::ir::FieldDef;
let mut config_ty = ty("ExtractionConfig", None);
config_ty.fields = vec![FieldDef {
name: "tree_sitter".to_string(),
cfg: Some("feature = \"tree-sitter\"".to_string()),
..FieldDef::default()
}];
let mut surface = ApiSurface::default();
surface.types.push(config_ty);
let filtered = surface.with_cfg_filtered_deep(&features(&["full"]));
assert_eq!(filtered.types[0].fields.len(), 1);
}
#[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);
}
}