widget_intelligence/
kyma_extractor.rs

1use crate::similarity_engine::Widget;
2use serde_json::Value;
3use std::collections::HashMap;
4
5pub struct KymaWidgetExtractor {
6    widget_descriptions: HashMap<i64, HashMap<String, Value>>,
7}
8
9impl KymaWidgetExtractor {
10    pub fn new() -> Self {
11        Self {
12            widget_descriptions: HashMap::new(),
13        }
14    }
15
16    pub fn cache_widget_description(&mut self, kyma_data: HashMap<String, Value>) {
17        if let Some(Value::Number(event_id)) = kyma_data.get("concreteEventID") {
18            if let Some(id) = event_id.as_i64() {
19                log::trace!("Caching widget description for event ID: {id}");
20                self.widget_descriptions.insert(id, kyma_data);
21            }
22        }
23    }
24
25    pub fn create_training_widget(&self, event_id: i64, current_value: f64) -> Option<Widget> {
26        let kyma_data = self.widget_descriptions.get(&event_id)?;
27
28        let widget = Widget {
29            label: self.extract_label(kyma_data),
30            minimum: self.extract_float_field(kyma_data, "minimum"),
31            maximum: self.extract_float_field(kyma_data, "maximum"),
32            current_value: Some(current_value),
33            is_generated: self.extract_bool_field(kyma_data, "isGenerated"),
34            display_type: self.extract_display_type(kyma_data),
35            event_id: Some(event_id as u64),
36            values: vec![current_value],
37        };
38
39        log::trace!(
40            "Created training widget for event ID {}: {:?}",
41            event_id,
42            widget.label
43        );
44        Some(widget)
45    }
46
47    pub fn get_cached_description(&self, event_id: i64) -> Option<&HashMap<String, Value>> {
48        self.widget_descriptions.get(&event_id)
49    }
50
51    pub fn get_cached_event_ids(&self) -> Vec<i64> {
52        self.widget_descriptions.keys().copied().collect()
53    }
54
55    pub fn clear_cache(&mut self) {
56        self.widget_descriptions.clear();
57    }
58
59    pub fn cache_size(&self) -> usize {
60        self.widget_descriptions.len()
61    }
62
63    pub fn extract_all_widgets_with_values(&self, values: &HashMap<i64, f64>) -> Vec<Widget> {
64        let mut widgets = Vec::new();
65
66        for (&event_id, &value) in values {
67            if let Some(widget) = self.create_training_widget(event_id, value) {
68                widgets.push(widget);
69            }
70        }
71
72        widgets
73    }
74
75    fn extract_label(&self, data: &HashMap<String, Value>) -> Option<String> {
76        if let Some(Value::String(label)) = data.get("label") {
77            if !label.is_empty() {
78                return Some(label.clone());
79            }
80        }
81
82        if let Some(Value::String(name)) = data.get("name") {
83            if !name.is_empty() {
84                return Some(name.clone());
85            }
86        }
87
88        if let Some(Value::String(title)) = data.get("title") {
89            if !title.is_empty() {
90                return Some(title.clone());
91            }
92        }
93
94        if let Some(Value::Number(event_id)) = data.get("concreteEventID") {
95            return Some(format!("Widget {event_id}"));
96        }
97
98        None
99    }
100
101    fn extract_display_type(&self, data: &HashMap<String, Value>) -> Option<String> {
102        if let Some(Value::String(display_type)) = data.get("displayType") {
103            return Some(display_type.clone());
104        }
105
106        if let Some(Value::String(widget_type)) = data.get("widgetType") {
107            return Some(widget_type.clone());
108        }
109
110        if let Some(Value::String(control_type)) = data.get("controlType") {
111            return Some(control_type.clone());
112        }
113
114        None
115    }
116
117    fn extract_float_field(&self, data: &HashMap<String, Value>, field_name: &str) -> Option<f64> {
118        if let Some(value) = data.get(field_name) {
119            match value {
120                Value::Number(n) => n.as_f64(),
121                Value::String(s) => s.parse::<f64>().ok(),
122                _ => None,
123            }
124        } else {
125            None
126        }
127    }
128
129    fn extract_bool_field(&self, data: &HashMap<String, Value>, field_name: &str) -> Option<bool> {
130        if let Some(value) = data.get(field_name) {
131            match value {
132                Value::Bool(b) => Some(*b),
133                Value::String(s) => match s.to_lowercase().as_str() {
134                    "true" | "1" | "yes" | "on" => Some(true),
135                    "false" | "0" | "no" | "off" => Some(false),
136                    _ => None,
137                },
138                Value::Number(n) => n.as_i64().map(|num| num != 0),
139                _ => None,
140            }
141        } else {
142            None
143        }
144    }
145
146    pub fn extract_widget_metadata(&self, event_id: i64) -> Option<WidgetMetadata> {
147        let kyma_data = self.widget_descriptions.get(&event_id)?;
148
149        Some(WidgetMetadata {
150            event_id,
151            label: self.extract_label(kyma_data),
152            display_type: self.extract_display_type(kyma_data),
153            minimum: self.extract_float_field(kyma_data, "minimum"),
154            maximum: self.extract_float_field(kyma_data, "maximum"),
155            default_value: self
156                .extract_float_field(kyma_data, "defaultValue")
157                .or_else(|| self.extract_float_field(kyma_data, "default")),
158            is_generated: self.extract_bool_field(kyma_data, "isGenerated"),
159            units: self.extract_string_field(kyma_data, "units"),
160            category: self.extract_string_field(kyma_data, "category"),
161            description: self.extract_string_field(kyma_data, "description"),
162        })
163    }
164
165    fn extract_string_field(
166        &self,
167        data: &HashMap<String, Value>,
168        field_name: &str,
169    ) -> Option<String> {
170        if let Some(Value::String(s)) = data.get(field_name) {
171            if !s.is_empty() {
172                Some(s.clone())
173            } else {
174                None
175            }
176        } else {
177            None
178        }
179    }
180
181    pub fn parse_kyma_json_string(json_str: &str) -> Result<HashMap<String, Value>, String> {
182        serde_json::from_str(json_str).map_err(|e| format!("Failed to parse JSON: {e}"))
183    }
184
185    pub fn validate_kyma_data(data: &HashMap<String, Value>) -> Result<(), String> {
186        if !data.contains_key("concreteEventID") {
187            return Err("Missing required field: concreteEventID".to_string());
188        }
189
190        if let Some(Value::Number(event_id)) = data.get("concreteEventID") {
191            if event_id.as_i64().is_none() {
192                return Err("concreteEventID must be a valid integer".to_string());
193            }
194        } else {
195            return Err("concreteEventID must be a number".to_string());
196        }
197
198        Ok(())
199    }
200}
201
202impl Default for KymaWidgetExtractor {
203    fn default() -> Self {
204        Self::new()
205    }
206}
207
208#[derive(Debug, Clone)]
209pub struct WidgetMetadata {
210    pub event_id: i64,
211    pub label: Option<String>,
212    pub display_type: Option<String>,
213    pub minimum: Option<f64>,
214    pub maximum: Option<f64>,
215    pub default_value: Option<f64>,
216    pub is_generated: Option<bool>,
217    pub units: Option<String>,
218    pub category: Option<String>,
219    pub description: Option<String>,
220}
221
222impl WidgetMetadata {
223    pub fn to_widget(&self, current_value: f64) -> Widget {
224        Widget {
225            label: self.label.clone(),
226            minimum: self.minimum,
227            maximum: self.maximum,
228            current_value: Some(current_value),
229            is_generated: self.is_generated,
230            display_type: self.display_type.clone(),
231            event_id: Some(self.event_id as u64),
232            values: vec![current_value],
233        }
234    }
235
236    pub fn is_valid_value(&self, value: f64) -> bool {
237        match (self.minimum, self.maximum) {
238            (Some(min), Some(max)) => value >= min && value <= max,
239            (Some(min), None) => value >= min,
240            (None, Some(max)) => value <= max,
241            (None, None) => true,
242        }
243    }
244
245    pub fn normalize_value(&self, value: f64) -> Option<f64> {
246        match (self.minimum, self.maximum) {
247            (Some(min), Some(max)) if max > min => Some((value - min) / (max - min)),
248            _ => None,
249        }
250    }
251
252    pub fn denormalize_value(&self, normalized_value: f64) -> Option<f64> {
253        match (self.minimum, self.maximum) {
254            (Some(min), Some(max)) if max > min => Some(min + normalized_value * (max - min)),
255            _ => None,
256        }
257    }
258}