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}