Skip to main content

endpoint_validator/tui/
state.rs

1use crate::parser::services::ConvertValue;
2use crate::parser::{EndpointMetadata, ParameterMetadata};
3use crate::ws::WsClient;
4use anyhow::{Context, Result, bail};
5use std::collections::HashMap;
6
7#[derive(PartialEq)]
8pub enum SettingsField {
9    Url,
10    Username,
11    Password,
12    ConnectButton,
13    DisconnectButton,
14}
15
16#[derive(PartialEq, Clone)]
17pub enum EndpointField {
18    Param(usize),
19    ConnectButton,
20    DisconnectButton,
21    JsonToggleButton,
22}
23
24#[derive(PartialEq)]
25pub enum AppBlock {
26    Settings,
27    EndpointList,
28    EndpointsReq,
29    EndpointsRes,
30}
31
32#[derive(PartialEq)]
33pub enum JsonViewMode {
34    Pretty,
35    Raw,
36}
37
38pub struct AppState {
39    pub client: Option<WsClient>,
40    pub current_block: AppBlock,
41    pub focused_settings_field: Option<SettingsField>,
42    pub focused_endpoint_field: Option<EndpointField>,
43    pub connected: bool,
44    pub endpoint_connected: bool,
45    pub url: String,
46    pub username: String,
47    pub password: String,
48    pub method_id: Option<u32>,
49    pub service_name: Option<String>,
50    pub params: Vec<ParameterMetadata>,
51    pub param_values: Vec<String>,
52    pub param_defaults: HashMap<String, HashMap<String, String>>,
53    pub json_view_mode: JsonViewMode,
54    pub json_data: Option<String>,
55    pub endpoints: Vec<String>,
56    pub selected_endpoint: usize,
57    pub endpoint_data: HashMap<String, EndpointMetadata>,
58    pub response_scroll: (u16, u16),
59    pub is_stream: bool,
60}
61
62impl AppState {
63    pub fn new(
64        endpoint_names: Vec<String>,
65        endpoint_data: HashMap<String, EndpointMetadata>,
66        param_defaults: HashMap<String, HashMap<String, String>>,
67    ) -> Self {
68        Self {
69            client: None,
70            current_block: AppBlock::Settings,
71            focused_settings_field: Some(SettingsField::Url),
72            focused_endpoint_field: Some(EndpointField::Param(0)),
73            connected: false,
74            endpoint_connected: false,
75            url: "ws://localhost:8443".to_string(),
76            username: String::new(),
77            password: String::new(),
78            method_id: None,
79            service_name: None,
80            params: Vec::new(),
81            param_values: Vec::new(),
82            param_defaults: param_defaults,
83            json_view_mode: JsonViewMode::Pretty,
84            json_data: None,
85            endpoints: endpoint_names,
86            selected_endpoint: 0,
87            endpoint_data: endpoint_data,
88            response_scroll: (0, 0),
89            is_stream: false,
90        }
91    }
92
93    // Scroll logic for the response section
94    pub fn scroll_response_down(&mut self) {
95        self.response_scroll.0 += 1;
96    }
97
98    pub fn scroll_response_up(&mut self) {
99        if self.response_scroll.0 > 0 {
100            self.response_scroll.0 -= 1;
101        }
102    }
103
104    pub fn scroll_response_right(&mut self) {
105        if self.current_block == AppBlock::EndpointsRes {
106            self.response_scroll.1 += 1;
107        }
108    }
109
110    pub fn scroll_response_left(&mut self) {
111        if self.current_block == AppBlock::EndpointsRes {
112            if self.response_scroll.1 > 0 {
113                self.response_scroll.1 -= 1;
114            }
115        }
116    }
117
118    // Handle user input
119    pub fn update_input(&mut self, c: char) {
120        match self.current_block {
121            AppBlock::Settings => self.update_settings_input(c),
122            AppBlock::EndpointsReq => self.update_endpoint_input(c),
123            _ => {}
124        }
125    }
126
127    fn update_settings_input(&mut self, c: char) {
128        match self.focused_settings_field {
129            Some(SettingsField::Url) => self.url.push(c),
130            Some(SettingsField::Username) => self.username.push(c),
131            Some(SettingsField::Password) => self.password.push(c),
132            _ => {}
133        }
134    }
135
136    fn update_endpoint_input(&mut self, c: char) {
137        if let Some(EndpointField::Param(index)) = self.focused_endpoint_field {
138            if let Some(param) = self.param_values.get_mut(index) {
139                param.push(c);
140            }
141        }
142    }
143
144    pub fn delete_last_char(&mut self) {
145        match self.current_block {
146            AppBlock::Settings => self.delete_last_char_from_settings(),
147            AppBlock::EndpointsReq => self.delete_last_char_from_endpoint(),
148            _ => {}
149        }
150    }
151
152    fn delete_last_char_from_settings(&mut self) {
153        match self.focused_settings_field {
154            Some(SettingsField::Url) => {
155                self.url.pop();
156            }
157            Some(SettingsField::Username) => {
158                self.username.pop();
159            }
160            Some(SettingsField::Password) => {
161                self.password.pop();
162            }
163            _ => {}
164        }
165    }
166
167    fn delete_last_char_from_endpoint(&mut self) {
168        if let Some(EndpointField::Param(index)) = self.focused_endpoint_field {
169            if let Some(param) = self.param_values.get_mut(index) {
170                param.pop();
171            }
172        }
173    }
174
175    // Navigation within blocks
176    pub fn next_field(&mut self) {
177        match self.current_block {
178            AppBlock::Settings => self.focused_settings_field = self.next_settings_field(),
179            AppBlock::EndpointList => self.select_next_endpoint(),
180            AppBlock::EndpointsReq => self.focused_endpoint_field = self.next_endpoint_field(),
181            AppBlock::EndpointsRes => self.scroll_response_down(),
182        }
183    }
184
185    pub fn previous_field(&mut self) {
186        match self.current_block {
187            AppBlock::Settings => self.focused_settings_field = self.previous_settings_field(),
188            AppBlock::EndpointList => self.select_previous_endpoint(),
189            AppBlock::EndpointsReq => self.focused_endpoint_field = self.previous_endpoint_field(),
190            AppBlock::EndpointsRes => self.scroll_response_up(),
191        }
192    }
193
194    fn next_settings_field(&self) -> Option<SettingsField> {
195        match self.focused_settings_field {
196            Some(SettingsField::Url) => Some(SettingsField::Username),
197            Some(SettingsField::Username) => Some(SettingsField::Password),
198            Some(SettingsField::Password) => Some(SettingsField::ConnectButton),
199            Some(SettingsField::ConnectButton) => Some(SettingsField::DisconnectButton),
200            Some(SettingsField::DisconnectButton) => Some(SettingsField::Url),
201            None => Some(SettingsField::Url),
202        }
203    }
204
205    fn previous_settings_field(&self) -> Option<SettingsField> {
206        match self.focused_settings_field {
207            Some(SettingsField::Url) => Some(SettingsField::DisconnectButton),
208            Some(SettingsField::Username) => Some(SettingsField::Url),
209            Some(SettingsField::Password) => Some(SettingsField::Username),
210            Some(SettingsField::ConnectButton) => Some(SettingsField::Password),
211            Some(SettingsField::DisconnectButton) => Some(SettingsField::ConnectButton),
212            None => Some(SettingsField::Url),
213        }
214    }
215
216    fn next_endpoint_field(&self) -> Option<EndpointField> {
217        match self.focused_endpoint_field {
218            Some(EndpointField::Param(index)) if index + 1 < self.params.len() => {
219                Some(EndpointField::Param(index + 1))
220            }
221            Some(EndpointField::Param(_)) => Some(EndpointField::ConnectButton),
222            Some(EndpointField::ConnectButton) => Some(EndpointField::DisconnectButton),
223            Some(EndpointField::DisconnectButton) => Some(EndpointField::JsonToggleButton),
224            Some(EndpointField::JsonToggleButton) | None => {
225                if self.params.is_empty() {
226                    Some(EndpointField::ConnectButton)
227                } else {
228                    Some(EndpointField::Param(0))
229                }
230            }
231        }
232    }
233
234    fn previous_endpoint_field(&self) -> Option<EndpointField> {
235        match self.focused_endpoint_field {
236            Some(EndpointField::Param(index)) if index > 0 => Some(EndpointField::Param(index - 1)),
237            Some(EndpointField::Param(_)) => Some(EndpointField::JsonToggleButton),
238            Some(EndpointField::JsonToggleButton) => Some(EndpointField::DisconnectButton),
239            Some(EndpointField::DisconnectButton) => Some(EndpointField::ConnectButton),
240            Some(EndpointField::ConnectButton) | None => {
241                if self.params.is_empty() {
242                    Some(EndpointField::JsonToggleButton)
243                } else {
244                    Some(EndpointField::Param(self.params.len() - 1))
245                }
246            }
247        }
248    }
249
250    // Block switching
251    pub fn switch_block(&mut self) {
252        if self.connected {
253            self.current_block = match self.current_block {
254                AppBlock::Settings => {
255                    self.update_selected_endpoint_data();
256                    AppBlock::EndpointList
257                }
258                AppBlock::EndpointList => {
259                    self.update_selected_endpoint_data();
260                    AppBlock::EndpointsReq
261                }
262                AppBlock::EndpointsReq => AppBlock::EndpointsRes,
263                AppBlock::EndpointsRes => AppBlock::Settings,
264            };
265        }
266    }
267
268    // Endpoint selection
269    pub fn select_next_endpoint(&mut self) {
270        if self.connected {
271            if self.selected_endpoint < self.endpoints.len() - 1 {
272                self.selected_endpoint += 1;
273                self.update_selected_endpoint_data();
274            }
275        }
276    }
277
278    pub fn select_previous_endpoint(&mut self) {
279        if self.connected {
280            if self.selected_endpoint > 0 {
281                self.selected_endpoint -= 1;
282                self.update_selected_endpoint_data();
283            }
284        }
285    }
286
287    fn update_selected_endpoint_data(&mut self) {
288        if let Some(endpoint) = self.endpoints.get(self.selected_endpoint) {
289            if let Some(metadata) = self.endpoint_data.get(endpoint) {
290                self.method_id = Some(metadata.method_id);
291                self.service_name = Some(metadata.service_name.clone());
292
293                // Do not sort params by their names
294                self.params = metadata.params.clone();
295
296                // Check if there are default values for this method_id
297                if let Some((_, defaults)) = self
298                    .param_defaults
299                    .iter()
300                    .find(|(id, _)| **id == metadata.method_id.to_string())
301                {
302                    // Create a map of default values for easy lookup
303                    let default_map: HashMap<_, _> = defaults.clone();
304
305                    // Populate param_values with either the default value or an empty string
306                    self.param_values = self
307                        .params
308                        .iter()
309                        .map(|param| {
310                            default_map
311                                .get(&param.name)
312                                .cloned()
313                                .unwrap_or_else(|| "".to_string())
314                        })
315                        .collect();
316                } else {
317                    // If no defaults are found, initialize with empty strings
318                    self.param_values = vec!["".to_string(); self.params.len()];
319                }
320
321                self.is_stream = metadata.is_stream;
322            }
323        }
324    }
325
326    pub fn toggle_json_view_mode(&mut self) {
327        if let Some(raw_json) = self.json_data.as_ref() {
328            self.json_view_mode = match self.json_view_mode {
329                JsonViewMode::Pretty => {
330                    let raw_json = serde_json::to_string(
331                        &serde_json::from_str::<serde_json::Value>(raw_json).unwrap_or_default(),
332                    )
333                    .unwrap_or_else(|_| raw_json.clone());
334                    self.json_data = Some(raw_json);
335                    JsonViewMode::Raw
336                }
337                JsonViewMode::Raw => {
338                    let pretty_json = serde_json::to_string_pretty(
339                        &serde_json::from_str::<serde_json::Value>(raw_json).unwrap_or_default(),
340                    )
341                    .unwrap_or_else(|_| raw_json.clone());
342                    self.json_data = Some(pretty_json);
343                    JsonViewMode::Pretty
344                }
345            };
346        }
347    }
348
349    // Handle connection and disconnection
350    pub async fn handle_enter(&mut self) -> Result<()> {
351        if self.current_block == AppBlock::Settings {
352            match self.focused_settings_field {
353                Some(SettingsField::ConnectButton) => {
354                    if let Err(_err) = self.handle_connect().await {
355                        self.connected = false;
356                        self.json_data = Some(_err.to_string());
357                    }
358                }
359                Some(SettingsField::DisconnectButton) => {
360                    if let Err(_err) = self.handle_disconnect().await {
361                        self.connected = true;
362                        self.json_data = Some(_err.to_string());
363                    }
364                }
365                _ => {}
366            }
367        } else if self.current_block == AppBlock::EndpointsReq {
368            match self.focused_endpoint_field {
369                Some(EndpointField::ConnectButton) => {
370                    if let Err(_err) = self.handle_endpoint_connect().await {
371                        self.endpoint_connected = false;
372                        self.json_data = Some(_err.to_string());
373                    }
374                }
375                Some(EndpointField::DisconnectButton) => {
376                    if let Err(_err) = self.handle_endpoint_disconnect().await {
377                        self.endpoint_connected = true;
378                        self.json_data = Some(_err.to_string());
379                    }
380                }
381                Some(EndpointField::JsonToggleButton) => self.toggle_json_view_mode(),
382                _ => {}
383            }
384        }
385
386        Ok(())
387    }
388
389    pub async fn handle_connect(&mut self) -> Result<()> {
390        let mut client = WsClient::new(&self.url, &format!("0loginstep1, 1{}", self.username))
391            .await
392            .context("Failed to connect to WebSocket")?;
393
394        let response = client
395            .recv_raw()
396            .await
397            .context("Failed to receive response from WebSocket for step 1")?;
398
399        let Ok(token) = response
400            .get("params")
401            .context("Missing params in response")
402            .and_then(|v| {
403                v.get("accessToken")
404                    .and_then(|token| token.as_str())
405                    .context("Missing accessToken in response")
406            })
407        else {
408            let response = self.format_json(&response)?;
409            bail!("Failed to get access token from response\n{response}");
410        };
411
412        let _ = client.close().await;
413        let mut client = WsClient::new(
414            &self.url,
415            &format!("0loginstep2, 1{token}, 2{}", self.password),
416        )
417        .await
418        .context("Failed to connect to WebSocket")?;
419
420        let response = client
421            .recv_raw()
422            .await
423            .context("Failed to receive response from WebSocket for step 2")?;
424        let response = self.format_json(&response)?;
425
426        self.json_data = Some(format!("Connected to {}\n{}", self.url, response));
427        self.client = Some(client);
428        self.connected = true;
429
430        Ok(())
431    }
432
433    pub async fn handle_disconnect(&mut self) -> Result<()> {
434        if let Some(client) = self.client.take() {
435            client.close().await?;
436        }
437        let resp = format!("Disconnected from to {}", self.url);
438        self.json_data = Some(resp);
439        self.connected = false;
440        Ok(())
441    }
442
443    pub async fn handle_endpoint_connect(&mut self) -> Result<()> {
444        let client = self
445            .client
446            .as_mut()
447            .context("WebSocket client is not connected")?;
448
449        let mut converted_params = Vec::new();
450        for (param, value) in self.params.iter().zip(self.param_values.iter()) {
451            let converted_value = param.ty.convert_value(value).context(format!(
452                "Failed to convert value for parameter: {}",
453                param.name
454            ))?;
455            converted_params.push(converted_value);
456        }
457
458        client
459            .send_req(self.method_id.unwrap(), converted_params)
460            .await
461            .context("Failed to send request to WebSocket")?;
462        let raw_response = client
463            .recv_raw()
464            .await
465            .context("Failed to receive response from WebSocket")?;
466
467        let resp = self.format_json(&raw_response)?;
468
469        self.endpoint_connected = true;
470        self.json_data = Some(resp);
471        Ok(())
472    }
473
474    pub async fn handle_endpoint_disconnect(&mut self) -> Result<()> {
475        self.endpoint_connected = false;
476        self.json_data = None;
477        Ok(())
478    }
479
480    fn format_json(&self, value: &serde_json::Value) -> Result<String> {
481        Ok(match self.json_view_mode {
482            JsonViewMode::Pretty => {
483                serde_json::to_string_pretty(value).context("Failed to format JSON as pretty")?
484            }
485            JsonViewMode::Raw => {
486                serde_json::to_string(value).context("Failed to format JSON as raw")?
487            }
488        })
489    }
490}