162 lines
5.3 KiB
Rust
162 lines
5.3 KiB
Rust
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<String>,
|
|
description: &'static str,
|
|
detail: &'static str,
|
|
source: &'static str,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
pub struct FeatureGroupResponse {
|
|
pub(crate) name: String,
|
|
pub(crate) features: Vec<FeatureInfo>,
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
pub struct FeaturesResponse {
|
|
pub groups: Vec<FeatureGroupResponse>,
|
|
}
|
|
|
|
/// 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<FeatureGroupResponse> = Vec::new();
|
|
|
|
for &group_name in &group_names {
|
|
let mut features: Vec<FeatureInfo> = 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<Arc<SharedState>>) -> Json<FeaturesResponse> {
|
|
let state = shared.load_state();
|
|
info!("GET /api/features");
|
|
Json(state.features_response.clone())
|
|
}
|