use anyhow::{bail, Result};
use crate::abi::{CarbonBudget, EnergyBudget, FunctionBudget};
#[derive(Debug, Clone)]
pub struct ParsedFunctionBudget {
pub name: String,
pub source: String,
pub energy_budget: Option<EnergyBudget>,
pub carbon_budget: Option<CarbonBudget>,
}
pub fn parse_function_budgets(functions: &[FunctionBudget]) -> Result<Vec<ParsedFunctionBudget>> {
let mut parsed = Vec::with_capacity(functions.len());
for func in functions {
let pf = parse_single_function(func)?;
parsed.push(pf);
}
Ok(parsed)
}
fn parse_single_function(func: &FunctionBudget) -> Result<ParsedFunctionBudget> {
let name = func.name.trim();
if name.is_empty() {
bail!("Function name cannot be empty");
}
if !is_valid_identifier(name) {
bail!(
"Function name '{}' is not a valid identifier (must be alphanumeric + underscores)",
name
);
}
let source = func.source.trim();
if source.is_empty() {
bail!("Function '{}' must have a non-empty source path", name);
}
if let Some(energy) = func.energy_budget_mj {
if energy < 0.0 {
bail!(
"Function '{}' has invalid energy budget: {} mJ (must be >= 0)",
name,
energy
);
}
}
if let Some(carbon) = func.carbon_budget_mg {
if carbon < 0.0 {
bail!(
"Function '{}' has invalid carbon budget: {} mg CO2 (must be >= 0)",
name,
carbon
);
}
}
Ok(ParsedFunctionBudget {
name: name.to_string(),
source: source.to_string(),
energy_budget: func.energy_budget_mj.map(EnergyBudget::new),
carbon_budget: func.carbon_budget_mg.map(CarbonBudget::new),
})
}
fn is_valid_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
let first = chars.next().unwrap();
if !first.is_alphabetic() && first != '_' {
return false;
}
for ch in chars {
if !ch.is_alphanumeric() && ch != '_' && ch != ':' {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::abi::FunctionBudget;
#[test]
fn test_parse_valid_function() {
let funcs = vec![FunctionBudget {
name: "process_batch".to_string(),
source: "src/batch.rs".to_string(),
energy_budget_mj: Some(50.0),
carbon_budget_mg: Some(10.0),
}];
let parsed = parse_function_budgets(&funcs).unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].name, "process_batch");
assert!(parsed[0].energy_budget.is_some());
assert!(parsed[0].carbon_budget.is_some());
}
#[test]
fn test_parse_invalid_name() {
let funcs = vec![FunctionBudget {
name: "123invalid".to_string(),
source: "src/lib.rs".to_string(),
energy_budget_mj: Some(10.0),
carbon_budget_mg: None,
}];
assert!(parse_function_budgets(&funcs).is_err());
}
#[test]
fn test_parse_path_qualified_name() {
let funcs = vec![FunctionBudget {
name: "module::process".to_string(),
source: "src/lib.rs".to_string(),
energy_budget_mj: Some(10.0),
carbon_budget_mg: None,
}];
let parsed = parse_function_budgets(&funcs).unwrap();
assert_eq!(parsed[0].name, "module::process");
}
#[test]
fn test_parse_negative_energy_budget() {
let funcs = vec![FunctionBudget {
name: "bad_func".to_string(),
source: "src/lib.rs".to_string(),
energy_budget_mj: Some(-1.0),
carbon_budget_mg: None,
}];
assert!(parse_function_budgets(&funcs).is_err());
}
#[test]
fn test_valid_identifiers() {
assert!(is_valid_identifier("process_batch"));
assert!(is_valid_identifier("_private"));
assert!(is_valid_identifier("module::func"));
assert!(!is_valid_identifier(""));
assert!(!is_valid_identifier("123abc"));
assert!(!is_valid_identifier("has space"));
}
}