widget_intelligence 2.0.0

A Rust library for intelligent Kyma widget suggestion and learning
Documentation
use crate::similarity_engine::Widget;
use serde_json::Value;
use std::collections::HashMap;

pub struct KymaWidgetExtractor {
    widget_descriptions: HashMap<i64, HashMap<String, Value>>,
}

impl KymaWidgetExtractor {
    pub fn new() -> Self {
        Self {
            widget_descriptions: HashMap::new(),
        }
    }

    pub fn cache_widget_description(&mut self, kyma_data: HashMap<String, Value>) {
        if let Some(Value::Number(event_id)) = kyma_data.get("concreteEventID") {
            if let Some(id) = event_id.as_i64() {
                log::trace!("Caching widget description for event ID: {id}");
                self.widget_descriptions.insert(id, kyma_data);
            }
        }
    }

    pub fn create_training_widget(&self, event_id: i64, current_value: f64) -> Option<Widget> {
        let kyma_data = self.widget_descriptions.get(&event_id)?;

        let widget = Widget {
            label: self.extract_label(kyma_data),
            minimum: self.extract_float_field(kyma_data, "minimum"),
            maximum: self.extract_float_field(kyma_data, "maximum"),
            current_value: Some(current_value),
            is_generated: self.extract_bool_field(kyma_data, "isGenerated"),
            display_type: self.extract_display_type(kyma_data),
            event_id: Some(event_id as u64),
            values: vec![current_value],
        };

        log::trace!(
            "Created training widget for event ID {}: {:?}",
            event_id,
            widget.label
        );
        Some(widget)
    }

    pub fn get_cached_description(&self, event_id: i64) -> Option<&HashMap<String, Value>> {
        self.widget_descriptions.get(&event_id)
    }

    pub fn get_cached_event_ids(&self) -> Vec<i64> {
        self.widget_descriptions.keys().copied().collect()
    }

    pub fn clear_cache(&mut self) {
        self.widget_descriptions.clear();
    }

    pub fn cache_size(&self) -> usize {
        self.widget_descriptions.len()
    }

    pub fn extract_all_widgets_with_values(&self, values: &HashMap<i64, f64>) -> Vec<Widget> {
        let mut widgets = Vec::new();

        for (&event_id, &value) in values {
            if let Some(widget) = self.create_training_widget(event_id, value) {
                widgets.push(widget);
            }
        }

        widgets
    }

    fn extract_label(&self, data: &HashMap<String, Value>) -> Option<String> {
        if let Some(Value::String(label)) = data.get("label") {
            if !label.is_empty() {
                return Some(label.clone());
            }
        }

        if let Some(Value::String(name)) = data.get("name") {
            if !name.is_empty() {
                return Some(name.clone());
            }
        }

        if let Some(Value::String(title)) = data.get("title") {
            if !title.is_empty() {
                return Some(title.clone());
            }
        }

        if let Some(Value::Number(event_id)) = data.get("concreteEventID") {
            return Some(format!("Widget {event_id}"));
        }

        None
    }

    fn extract_display_type(&self, data: &HashMap<String, Value>) -> Option<String> {
        if let Some(Value::String(display_type)) = data.get("displayType") {
            return Some(display_type.clone());
        }

        if let Some(Value::String(widget_type)) = data.get("widgetType") {
            return Some(widget_type.clone());
        }

        if let Some(Value::String(control_type)) = data.get("controlType") {
            return Some(control_type.clone());
        }

        None
    }

    fn extract_float_field(&self, data: &HashMap<String, Value>, field_name: &str) -> Option<f64> {
        if let Some(value) = data.get(field_name) {
            match value {
                Value::Number(n) => n.as_f64(),
                Value::String(s) => s.parse::<f64>().ok(),
                _ => None,
            }
        } else {
            None
        }
    }

    fn extract_bool_field(&self, data: &HashMap<String, Value>, field_name: &str) -> Option<bool> {
        if let Some(value) = data.get(field_name) {
            match value {
                Value::Bool(b) => Some(*b),
                Value::String(s) => match s.to_lowercase().as_str() {
                    "true" | "1" | "yes" | "on" => Some(true),
                    "false" | "0" | "no" | "off" => Some(false),
                    _ => None,
                },
                Value::Number(n) => n.as_i64().map(|num| num != 0),
                _ => None,
            }
        } else {
            None
        }
    }

    pub fn extract_widget_metadata(&self, event_id: i64) -> Option<WidgetMetadata> {
        let kyma_data = self.widget_descriptions.get(&event_id)?;

        Some(WidgetMetadata {
            event_id,
            label: self.extract_label(kyma_data),
            display_type: self.extract_display_type(kyma_data),
            minimum: self.extract_float_field(kyma_data, "minimum"),
            maximum: self.extract_float_field(kyma_data, "maximum"),
            default_value: self
                .extract_float_field(kyma_data, "defaultValue")
                .or_else(|| self.extract_float_field(kyma_data, "default")),
            is_generated: self.extract_bool_field(kyma_data, "isGenerated"),
            units: self.extract_string_field(kyma_data, "units"),
            category: self.extract_string_field(kyma_data, "category"),
            description: self.extract_string_field(kyma_data, "description"),
        })
    }

    fn extract_string_field(
        &self,
        data: &HashMap<String, Value>,
        field_name: &str,
    ) -> Option<String> {
        if let Some(Value::String(s)) = data.get(field_name) {
            if !s.is_empty() {
                Some(s.clone())
            } else {
                None
            }
        } else {
            None
        }
    }

    pub fn parse_kyma_json_string(json_str: &str) -> Result<HashMap<String, Value>, String> {
        serde_json::from_str(json_str).map_err(|e| format!("Failed to parse JSON: {e}"))
    }

    pub fn validate_kyma_data(data: &HashMap<String, Value>) -> Result<(), String> {
        if !data.contains_key("concreteEventID") {
            return Err("Missing required field: concreteEventID".to_string());
        }

        if let Some(Value::Number(event_id)) = data.get("concreteEventID") {
            if event_id.as_i64().is_none() {
                return Err("concreteEventID must be a valid integer".to_string());
            }
        } else {
            return Err("concreteEventID must be a number".to_string());
        }

        Ok(())
    }
}

impl Default for KymaWidgetExtractor {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Debug, Clone)]
pub struct WidgetMetadata {
    pub event_id: i64,
    pub label: Option<String>,
    pub display_type: Option<String>,
    pub minimum: Option<f64>,
    pub maximum: Option<f64>,
    pub default_value: Option<f64>,
    pub is_generated: Option<bool>,
    pub units: Option<String>,
    pub category: Option<String>,
    pub description: Option<String>,
}

impl WidgetMetadata {
    pub fn to_widget(&self, current_value: f64) -> Widget {
        Widget {
            label: self.label.clone(),
            minimum: self.minimum,
            maximum: self.maximum,
            current_value: Some(current_value),
            is_generated: self.is_generated,
            display_type: self.display_type.clone(),
            event_id: Some(self.event_id as u64),
            values: vec![current_value],
        }
    }

    pub fn is_valid_value(&self, value: f64) -> bool {
        match (self.minimum, self.maximum) {
            (Some(min), Some(max)) => value >= min && value <= max,
            (Some(min), None) => value >= min,
            (None, Some(max)) => value <= max,
            (None, None) => true,
        }
    }

    pub fn normalize_value(&self, value: f64) -> Option<f64> {
        match (self.minimum, self.maximum) {
            (Some(min), Some(max)) if max > min => Some((value - min) / (max - min)),
            _ => None,
        }
    }

    pub fn denormalize_value(&self, normalized_value: f64) -> Option<f64> {
        match (self.minimum, self.maximum) {
            (Some(min), Some(max)) if max > min => Some(min + normalized_value * (max - min)),
            _ => None,
        }
    }
}