use std::collections::BTreeSet;
use harn_parser::{Attribute, Node};
use super::{Algorithm, BudgetSpec, Quota, RouteLimits};
pub fn limits_and_budget_from_attributes(
attrs: &[Attribute],
) -> (Option<RouteLimits>, Option<BudgetSpec>) {
let mut limits: Option<RouteLimits> = None;
let mut budget: Option<BudgetSpec> = None;
for attr in attrs {
match attr.name.as_str() {
"limits" => limits = Some(parse_limits(attr, limits.take())),
"budget" => budget = Some(parse_budget(attr, budget.take())),
_ => continue,
}
}
(limits, budget)
}
fn parse_limits(attr: &Attribute, base: Option<RouteLimits>) -> RouteLimits {
let mut limits = base.unwrap_or_default();
for arg in &attr.args {
let Some(key) = arg.name.as_deref() else {
continue;
};
match key {
"per_tenant" => {
if let Some(quota) = string_value(arg).and_then(Quota::parse) {
limits.per_tenant = Some(quota);
}
}
"per_scope" => {
if let Some(quota) = string_value(arg).and_then(Quota::parse) {
limits.per_scope = Some(quota);
}
}
"per_route" => {
if let Some(quota) = string_value(arg).and_then(Quota::parse) {
limits.per_route = Some(quota);
}
}
"burst" => {
if let Some(value) = int_value(arg) {
limits.burst = Some(value.max(1) as u32);
}
}
"algorithm" => {
if let Some(algo) = string_value(arg).as_deref().and_then(Algorithm::parse) {
limits.algorithm = algo;
}
}
"in_flight_max" => {
if let Some(value) = int_value(arg) {
limits.in_flight_max = Some(value.max(1) as u32);
}
}
_ => continue,
}
}
limits
}
fn parse_budget(attr: &Attribute, base: Option<BudgetSpec>) -> BudgetSpec {
let mut budget = base.unwrap_or_default();
for arg in &attr.args {
let Some(key) = arg.name.as_deref() else {
continue;
};
match key {
"llm_cost_usd" => {
if let Some(value) = float_value(arg) {
budget.llm_cost_usd = Some(value.max(0.0));
}
}
"llm_tokens" => {
if let Some(value) = int_value(arg) {
budget.llm_tokens = Some(value.max(0) as u64);
}
}
"pg_queries" => {
if let Some(value) = int_value(arg) {
budget.pg_queries = Some(value.max(0) as u64);
}
}
"mcp_calls" => {
if let Some(value) = int_value(arg) {
budget.mcp_calls = Some(value.max(0) as u64);
}
}
_ => continue,
}
}
budget
}
fn string_value(arg: &harn_parser::AttributeArg) -> Option<String> {
match &arg.value.node {
Node::StringLiteral(s) | Node::RawStringLiteral(s) => Some(s.clone()),
_ => None,
}
}
fn int_value(arg: &harn_parser::AttributeArg) -> Option<i64> {
match &arg.value.node {
Node::IntLiteral(n) => Some(*n),
Node::FloatLiteral(f) if f.is_finite() && f.fract() == 0.0 => Some(*f as i64),
_ => None,
}
}
fn float_value(arg: &harn_parser::AttributeArg) -> Option<f64> {
match &arg.value.node {
Node::FloatLiteral(f) => Some(*f),
Node::IntLiteral(n) => Some(*n as f64),
_ => None,
}
}
pub fn collect_scope_set(scopes: &BTreeSet<String>) -> Vec<String> {
scopes.iter().cloned().collect()
}