use crate::abi::{ComplexityScore, DisclosureLevel};
use crate::codegen::parser::parse_endpoint_params;
use crate::manifest::{EndpointDef, LevelConfig};
const WEIGHT_PARAM_COUNT: f64 = 0.30;
const WEIGHT_OPTIONAL_RATIO: f64 = 0.25;
const WEIGHT_TYPE_COMPLEXITY: f64 = 0.25;
const WEIGHT_REQUIRED_COUNT: f64 = 0.20;
const MAX_PARAM_COUNT: f64 = 10.0;
const MAX_TYPE_COMPLEXITY: f64 = 20.0;
const MAX_REQUIRED_COUNT: f64 = 8.0;
pub fn score_endpoint(endpoint: &EndpointDef) -> ComplexityScore {
let params = parse_endpoint_params(endpoint);
if params.is_empty() {
return ComplexityScore { value: 0 };
}
let total = params.len() as f64;
let optional_count = params.iter().filter(|p| p.optional).count() as f64;
let required_count = total - optional_count;
let type_complexity_sum: f64 = params
.iter()
.map(|p| p.param_type.complexity_weight() as f64)
.sum();
let param_factor = (total / MAX_PARAM_COUNT).min(1.0);
let optional_factor = if total > 0.0 {
optional_count / total
} else {
0.0
};
let type_factor = (type_complexity_sum / MAX_TYPE_COMPLEXITY).min(1.0);
let required_factor = (required_count / MAX_REQUIRED_COUNT).min(1.0);
let raw_score = (param_factor * WEIGHT_PARAM_COUNT
+ optional_factor * WEIGHT_OPTIONAL_RATIO
+ type_factor * WEIGHT_TYPE_COMPLEXITY
+ required_factor * WEIGHT_REQUIRED_COUNT)
* 100.0;
let clamped = raw_score.round().min(100.0).max(0.0) as u32;
ComplexityScore { value: clamped }
}
pub fn assign_level(score: &ComplexityScore, levels: &LevelConfig) -> DisclosureLevel {
if score.value <= levels.beginner_threshold {
DisclosureLevel::Beginner
} else if score.value >= levels.expert_threshold {
DisclosureLevel::Expert
} else {
DisclosureLevel::Intermediate
}
}
pub fn score_all_endpoints(
endpoints: &[EndpointDef],
levels: &LevelConfig,
) -> Vec<(String, ComplexityScore, DisclosureLevel)> {
endpoints
.iter()
.map(|ep| {
let score = score_endpoint(ep);
let level = assign_level(&score, levels);
(ep.name.clone(), score, level)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::EndpointDef;
#[test]
fn test_simple_endpoint_scores_low() {
let ep = EndpointDef {
name: "ping".to_string(),
params: vec!["host: string".to_string(), "timeout: int".to_string()],
required: vec!["host".to_string(), "timeout".to_string()],
description: String::new(),
};
let score = score_endpoint(&ep);
assert!(
score.value <= 30,
"simple 2-param endpoint should be beginner-level, got {}",
score.value
);
}
#[test]
fn test_complex_endpoint_scores_high() {
let ep = EndpointDef {
name: "search".to_string(),
params: vec![
"query: string".to_string(),
"filters?: map".to_string(),
"sort?: string".to_string(),
"page?: int".to_string(),
"limit?: int".to_string(),
"cursor?: string".to_string(),
"facets?: list".to_string(),
],
required: vec!["query".to_string()],
description: String::new(),
};
let score = score_endpoint(&ep);
assert!(
score.value > 50,
"complex 7-param endpoint should be intermediate+, got {}",
score.value
);
}
#[test]
fn test_level_assignment_beginner() {
let levels = LevelConfig::default();
let score = ComplexityScore { value: 20 };
assert_eq!(assign_level(&score, &levels), DisclosureLevel::Beginner);
}
#[test]
fn test_level_assignment_intermediate() {
let levels = LevelConfig::default();
let score = ComplexityScore { value: 50 };
assert_eq!(
assign_level(&score, &levels),
DisclosureLevel::Intermediate
);
}
#[test]
fn test_level_assignment_expert() {
let levels = LevelConfig::default();
let score = ComplexityScore { value: 85 };
assert_eq!(assign_level(&score, &levels), DisclosureLevel::Expert);
}
}