pub mod default_value_for_enum;
pub mod extractor;
mod type_resolver;
pub mod validation;
use crate::core::ir::{ApiSurface, FieldDef, TypeRef};
#[derive(Debug, Clone)]
pub struct FieldInfo {
pub name: String,
pub ty: TypeRef,
pub optional: bool,
}
impl From<&FieldDef> for FieldInfo {
fn from(f: &FieldDef) -> Self {
Self {
name: f.name.clone(),
ty: f.ty.clone(),
optional: f.optional,
}
}
}
pub fn return_type_fields(surface: &ApiSurface, function_name: &str) -> Option<Vec<FieldInfo>> {
let func = surface.functions.iter().find(|f| f.name == function_name)?;
let type_name = match &func.return_type {
TypeRef::Named(name) => name.as_str(),
_ => return None,
};
let type_def = surface.types.iter().find(|t| t.name == type_name)?;
if type_def.fields.is_empty() {
return None;
}
Some(type_def.fields.iter().map(FieldInfo::from).collect())
}
#[derive(Debug)]
pub enum ExportValidation {
Ok,
WrongPath {
function: String,
declared_module: String,
actual_paths: Vec<String>,
},
NotFound { function: String },
}
pub fn validate_call_export(surface: &ApiSurface, module_path: &str, function_name: &str) -> ExportValidation {
let expected_rust_path = format!("{module_path}::{function_name}");
let mut all_defs: Vec<String> = surface
.functions
.iter()
.filter(|f| f.name == function_name)
.map(|f| f.rust_path.clone())
.collect();
let mut method_match_found = false;
for type_def in &surface.types {
for method in &type_def.methods {
if method.name == function_name {
method_match_found = true;
all_defs.push(format!("{}::{}", type_def.rust_path, method.name));
}
}
}
if all_defs.is_empty() {
return ExportValidation::NotFound {
function: function_name.to_string(),
};
}
if all_defs.iter().any(|p| p == &expected_rust_path) {
return ExportValidation::Ok;
}
if method_match_found {
return ExportValidation::Ok;
}
ExportValidation::WrongPath {
function: function_name.to_string(),
declared_module: module_path.to_string(),
actual_paths: all_defs,
}
}
#[cfg(test)]
mod tests {
use crate::core::ir::{ApiSurface, FieldDef, MethodDef, TypeDef, TypeRef};
use super::{ExportValidation, return_type_fields, validate_call_export};
fn make_method(name: &str, return_type: TypeRef) -> MethodDef {
MethodDef {
name: name.to_string(),
params: vec![],
return_type,
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: None,
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
}
}
fn make_fn(name: &str, rust_path: &str, return_type: TypeRef) -> crate::core::ir::FunctionDef {
crate::core::ir::FunctionDef {
name: name.to_string(),
rust_path: rust_path.to_string(),
original_rust_path: String::new(),
params: vec![],
return_type,
is_async: false,
error_type: None,
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}
}
fn make_type(name: &str, fields: Vec<FieldDef>) -> TypeDef {
TypeDef {
name: name.to_string(),
rust_path: format!("my_crate::{name}"),
original_rust_path: String::new(),
fields,
methods: vec![],
is_opaque: false,
is_clone: false,
is_copy: false,
is_trait: false,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
doc: String::new(),
cfg: None,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
}
}
fn make_field(name: &str, ty: TypeRef) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
optional: false,
default: None,
doc: String::new(),
sanitized: false,
is_boxed: false,
type_rust_path: None,
cfg: None,
typed_default: None,
core_wrapper: crate::core::ir::CoreWrapper::None,
vec_inner_core_wrapper: crate::core::ir::CoreWrapper::None,
newtype_wrapper: None,
serde_rename: None,
serde_flatten: false,
binding_excluded: false,
binding_exclusion_reason: None,
original_type: None,
}
}
fn empty_surface() -> ApiSurface {
ApiSurface {
crate_name: "my_crate".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
}
}
#[test]
fn validate_ok_when_exported_at_declared_module() {
let mut surface = empty_surface();
surface
.functions
.push(make_fn("render_page", "my_crate::render_page", TypeRef::String));
assert!(matches!(
validate_call_export(&surface, "my_crate", "render_page"),
ExportValidation::Ok
));
}
#[test]
fn validate_wrong_path_when_not_re_exported_at_root() {
let mut surface = empty_surface();
surface.functions.push(make_fn(
"render_page",
"my_crate::rendering::render_page",
TypeRef::String,
));
match validate_call_export(&surface, "my_crate", "render_page") {
ExportValidation::WrongPath {
function,
declared_module,
actual_paths,
} => {
assert_eq!(function, "render_page");
assert_eq!(declared_module, "my_crate");
assert!(actual_paths.contains(&"my_crate::rendering::render_page".to_string()));
}
other => panic!("expected WrongPath, got {other:?}"),
}
}
#[test]
fn validate_not_found_when_function_absent() {
let surface = empty_surface();
match validate_call_export(&surface, "my_crate", "missing_fn") {
ExportValidation::NotFound { function } => assert_eq!(function, "missing_fn"),
other => panic!("expected NotFound, got {other:?}"),
}
}
#[test]
fn validate_wrong_path_when_both_definitions_are_deep() {
let mut surface = empty_surface();
surface.functions.push(make_fn(
"clean_extracted_text",
"my_crate::utils::quality::clean_extracted_text",
TypeRef::String,
));
surface.functions.push(make_fn(
"clean_extracted_text",
"my_crate::text::quality::clean_extracted_text",
TypeRef::String,
));
match validate_call_export(&surface, "my_crate", "clean_extracted_text") {
ExportValidation::WrongPath { actual_paths, .. } => assert_eq!(actual_paths.len(), 2),
other => panic!("expected WrongPath, got {other:?}"),
}
}
#[test]
fn validate_ok_when_one_definition_is_at_root() {
let mut surface = empty_surface();
surface.functions.push(make_fn(
"normalize_whitespace",
"my_crate::normalize_whitespace",
TypeRef::String,
));
assert!(matches!(
validate_call_export(&surface, "my_crate", "normalize_whitespace"),
ExportValidation::Ok
));
}
#[test]
fn return_type_fields_returns_struct_fields() {
let mut surface = empty_surface();
surface.functions.push(make_fn(
"extract_doc",
"my_crate::extract_doc",
TypeRef::Named("ExtractionResult".into()),
));
surface.types.push(make_type(
"ExtractionResult",
vec![
make_field("content", TypeRef::String),
make_field("mime_type", TypeRef::String),
],
));
let fields = return_type_fields(&surface, "extract_doc").expect("should return fields");
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].name, "content");
assert_eq!(fields[1].name, "mime_type");
}
#[test]
fn return_type_fields_none_for_primitive_return() {
let mut surface = empty_surface();
surface.functions.push(make_fn(
"count_words",
"my_crate::count_words",
TypeRef::Primitive(crate::core::ir::PrimitiveType::U64),
));
assert!(return_type_fields(&surface, "count_words").is_none());
}
#[test]
fn return_type_fields_none_for_string_return() {
let mut surface = empty_surface();
surface
.functions
.push(make_fn("get_name", "my_crate::get_name", TypeRef::String));
assert!(return_type_fields(&surface, "get_name").is_none());
}
#[test]
fn return_type_fields_none_for_unknown_function() {
let surface = empty_surface();
assert!(return_type_fields(&surface, "nonexistent").is_none());
}
#[test]
fn return_type_fields_none_for_opaque_type() {
let mut surface = empty_surface();
surface.functions.push(make_fn(
"make_handle",
"my_crate::make_handle",
TypeRef::Named("Handle".into()),
));
surface.types.push(make_type("Handle", vec![]));
assert!(return_type_fields(&surface, "make_handle").is_none());
}
fn extract_from_tmp(lib_rs_source: &str) -> ApiSurface {
let dir = tempfile::tempdir().expect("tempdir");
let lib_rs = dir.path().join("lib.rs");
std::fs::write(&lib_rs, lib_rs_source).expect("write lib.rs");
crate::extract::extractor::extract(&[lib_rs.as_path()], "sample_crate", "0.0.0", None).expect("extract failed")
}
#[test]
fn e2e_c1_deep_path_caught_by_validate() {
let surface = extract_from_tmp(
r#"
pub mod rendering {
pub fn render_page(page: u32) -> String { todo!() }
}
"#,
);
match validate_call_export(&surface, "sample_crate", "render_page") {
ExportValidation::WrongPath { actual_paths, .. } => {
assert!(actual_paths.iter().any(|p| p.contains("rendering")));
}
other => panic!("expected WrongPath, got {other:?}"),
}
}
#[test]
fn e2e_c1_glob_reexport_resolves_ok() {
let surface = extract_from_tmp(
r#"
pub use rendering::*;
pub mod rendering {
pub fn render_page(page: u32) -> String { todo!() }
}
"#,
);
assert!(matches!(
validate_call_export(&surface, "sample_crate", "render_page"),
ExportValidation::Ok
));
}
#[test]
fn validate_ok_when_method_matches() {
let mut surface = empty_surface();
let mut client = make_type("DefaultClient", vec![]);
client.methods.push(make_method("chat", TypeRef::String));
surface.types.push(client);
assert!(matches!(
validate_call_export(&surface, "my_crate", "chat"),
ExportValidation::Ok
));
}
#[test]
fn validate_not_found_when_neither_function_nor_method_matches() {
let mut surface = empty_surface();
let mut client = make_type("DefaultClient", vec![]);
client.methods.push(make_method("complete", TypeRef::String));
surface.types.push(client);
match validate_call_export(&surface, "my_crate", "chat") {
ExportValidation::NotFound { function } => assert_eq!(function, "chat"),
other => panic!("expected NotFound, got {other:?}"),
}
}
#[test]
fn e2e_c1_named_reexport_resolves_ok() {
let surface = extract_from_tmp(
r#"
pub use chunking::core::chunk_text;
pub mod chunking {
pub mod core {
pub async fn chunk_text(text: String) -> Vec<String> { todo!() }
}
}
"#,
);
assert!(matches!(
validate_call_export(&surface, "sample_crate", "chunk_text"),
ExportValidation::Ok
));
}
}