use std::sync::Arc; use axum::extract::State; use axum::response::Json; use serde::Serialize; use tracing::info; use crate::data::{Histogram, PropertyData}; use crate::features::{ENUM_FEATURE_GROUPS, FEATURE_GROUPS}; use crate::state::SharedState; fn is_empty(val: &str) -> bool { val.is_empty() } fn is_false(val: &bool) -> bool { !val } fn is_empty_slice(val: &&[&str]) -> bool { val.is_empty() } #[derive(Clone, 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(skip_serializing_if = "is_empty")] prefix: &'static str, #[serde(skip_serializing_if = "is_empty")] suffix: &'static str, #[serde(skip_serializing_if = "is_false")] raw: bool, #[serde(skip_serializing_if = "is_false")] absolute: bool, #[serde(skip_serializing_if = "is_empty_slice")] modes: &'static [&'static str], #[serde(skip_serializing_if = "is_empty")] linked: &'static str, }, #[serde(rename = "enum")] Enum { name: String, values: Vec, description: &'static str, detail: &'static str, source: &'static str, }, } #[derive(Clone, Serialize)] pub struct FeatureGroupResponse { pub(crate) name: String, pub(crate) features: Vec, } #[derive(Clone, Serialize)] pub struct FeaturesResponse { pub groups: Vec, } /// Build the features response at startup. Called once and cached in AppState. pub fn build_features_response(data: &PropertyData) -> FeaturesResponse { // 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) = data .feature_names .iter() .position(|feat_name| feat_name == feature_config.name) { let stats = &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, prefix: feature_config.prefix, suffix: feature_config.suffix, raw: feature_config.raw, absolute: feature_config.absolute, modes: feature_config.modes, linked: feature_config.linked, }); } } } } // 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 { // Find the feature index by name if let Some(feat_idx) = data .feature_names .iter() .position(|name| name == enum_config.name) { // Check if this feature has enum values if let Some(values) = data.enum_values.get(&feat_idx) { features.push(FeatureInfo::Enum { name: enum_config.name.to_string(), values: 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, }); } } FeaturesResponse { groups } } pub async fn get_features(State(shared): State>) -> Json { let state = shared.load_state(); info!("GET /api/features"); Json(state.features_response.clone()) }