use std::sync::Arc; use axum::response::Json; use serde::Serialize; use tracing::info; use crate::data::Histogram; use crate::features::{ENUM_FEATURE_GROUPS, FEATURE_GROUPS}; use crate::state::AppState; #[derive(Serialize)] #[serde(tag = "type")] pub enum FeatureInfo { #[serde(rename = "numeric")] Numeric { name: String, min: f32, max: f32, step: f32, histogram: Histogram, description: &'static str, detail: &'static str, source: &'static str, }, #[serde(rename = "enum")] Enum { name: String, values: Vec, description: &'static str, detail: &'static str, source: &'static str, }, } #[derive(Serialize)] pub struct FeatureGroupResponse { name: String, features: Vec, } #[derive(Serialize)] pub struct FeaturesResponse { groups: Vec, } pub async fn get_features(state: Arc) -> Json { // Collect all group names in order, merging numeric and enum groups with the same name let mut group_names: Vec<&str> = Vec::new(); for feature_group in FEATURE_GROUPS { if !group_names.contains(&feature_group.name) { group_names.push(feature_group.name); } } for enum_group in ENUM_FEATURE_GROUPS { if !group_names.contains(&enum_group.name) { group_names.push(enum_group.name); } } let mut groups: Vec = Vec::new(); for &group_name in &group_names { let mut features: Vec = Vec::new(); // Add numeric features for this group for feature_group in FEATURE_GROUPS { if feature_group.name == group_name { for feature_config in feature_group.features { if let Some(feat_idx) = state.data.feature_names.iter().position(|feat_name| feat_name == feature_config.name) { let stats = &state.data.feature_stats[feat_idx]; features.push(FeatureInfo::Numeric { name: feature_config.name.to_string(), min: stats.slider_min, max: stats.slider_max, step: feature_config.step, histogram: stats.histogram.clone(), description: feature_config.description, detail: feature_config.detail, source: feature_config.source, }); } } } } // Add enum features for this group for enum_group in ENUM_FEATURE_GROUPS { if enum_group.name == group_name { for enum_config in enum_group.features { if let Some(enum_feature) = state .data .enum_features .iter() .find(|enum_feat| enum_feat.name == enum_config.name) { features.push(FeatureInfo::Enum { name: enum_config.name.to_string(), values: enum_feature.values.clone(), description: enum_config.description, detail: enum_config.detail, source: enum_config.source, }); } } } } if !features.is_empty() { groups.push(FeatureGroupResponse { name: group_name.to_string(), features, }); } } let num_numeric: usize = groups .iter() .flat_map(|group| &group.features) .filter(|feature| matches!(feature, FeatureInfo::Numeric { .. })) .count(); let num_enum: usize = groups .iter() .flat_map(|group| &group.features) .filter(|feature| matches!(feature, FeatureInfo::Enum { .. })) .count(); info!( numeric = num_numeric, enums = num_enum, groups = groups.len(), "GET /api/features" ); Json(FeaturesResponse { groups }) }