use std::{str::FromStr, sync::Arc};
use color_eyre::eyre::Result;
use crossterm::event::{Event, KeyCode, KeyEvent};
use openapi_31::v31::parameter::In;
use ratatui::{prelude::*, widgets::*};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use tui_input::{backend::crossterm::EventHandler, Input};
use crate::{
action::Action,
pages::phone::{RequestBuilder, RequestPane},
panes::Pane,
state::{InputMode, OperationItem, State},
tui::{EventResponse, Frame},
};
pub struct ParameterEditor {
focused: bool,
focused_border_style: Style,
operation_item: Arc<OperationItem>,
parameters: Vec<ParameterTab>,
selected_parameter: usize,
input: Input,
}
#[derive(Default)]
pub struct ParameterItem {
pub name: String,
pub value: Option<String>,
pub required: bool,
pub schema: Option<serde_json::Value>,
}
#[derive(Default)]
pub struct ParameterTab {
pub location: String,
pub items: Vec<ParameterItem>,
pub table_state: TableState,
}
impl ParameterEditor {
pub fn new(operation_item: Arc<OperationItem>, focused: bool, focused_border_style: Style) -> Self {
Self {
operation_item,
focused,
focused_border_style,
parameters: vec![],
selected_parameter: 0,
input: Input::default(),
}
}
fn border_style(&self) -> Style {
match self.focused {
true => self.focused_border_style,
false => Style::default(),
}
}
fn border_type(&self) -> BorderType {
match self.focused {
true => BorderType::Thick,
false => BorderType::Plain,
}
}
fn location_color(&self, status: &str) -> Color {
if status.eq_ignore_ascii_case("header") {
return Color::LightCyan;
}
if status.eq_ignore_ascii_case("path") {
return Color::LightBlue;
}
if status.eq_ignore_ascii_case("query") {
return Color::LightMagenta;
}
if status.eq_ignore_ascii_case("cookie") {
return Color::LightRed;
}
Color::default()
}
fn init_parameters(&mut self, state: &State) -> Result<()> {
{
let mut path_items = vec![];
let mut query_items = vec![];
let mut header_items = vec![];
let mut cookie_items = vec![];
self.operation_item.operation.parameters.iter().flatten().for_each(|parameter_or_ref| {
let parameter = parameter_or_ref.resolve(&state.openapi_spec).unwrap();
let value =
parameter.schema.clone().and_then(|schema| schema.get("default").map(|default| default.to_string()));
match parameter.r#in {
In::Query => &mut query_items,
In::Header => &mut header_items,
In::Path => &mut path_items,
In::Cookie => &mut cookie_items,
}
.push(ParameterItem {
name: parameter.name.clone(),
value,
required: parameter.required.unwrap_or(false),
schema: parameter.schema.clone(),
});
});
if !path_items.is_empty() {
self.parameters.push(ParameterTab {
location: "Path".to_string(),
items: path_items,
table_state: TableState::default().with_selected(0),
});
}
self.parameters.push(ParameterTab {
location: "Query".to_string(),
items: query_items,
table_state: TableState::default().with_selected(0),
});
self.parameters.push(ParameterTab {
location: "Header".to_string(),
items: header_items,
table_state: TableState::default().with_selected(0),
});
if !cookie_items.is_empty() {
self.parameters.push(ParameterTab {
location: "Cookie".to_string(),
items: cookie_items,
table_state: TableState::default().with_selected(0),
});
}
}
Ok(())
}
fn select_parameters<'a>(&'a self, parameter_type: &'a str) -> impl Iterator<Item = &'a ParameterItem> + 'a {
self
.parameters
.iter()
.filter_map(
|parameter| {
if parameter.location.eq_ignore_ascii_case(parameter_type) {
Some(¶meter.items)
} else {
None
}
},
)
.flatten()
}
fn path_parameters(&self) -> impl Iterator<Item = &ParameterItem> {
self.select_parameters("path")
}
fn query_parameters(&self) -> impl Iterator<Item = &ParameterItem> {
self.select_parameters("query")
}
fn header_parameters(&self) -> impl Iterator<Item = &ParameterItem> {
self.select_parameters("header")
}
#[allow(dead_code)]
fn cookie_parameters(&self) -> impl Iterator<Item = &ParameterItem> {
self.select_parameters("cookie")
}
}
impl RequestPane for ParameterEditor {}
impl RequestBuilder for ParameterEditor {
fn path(&self, url: String) -> String {
self.path_parameters().fold(url, |url, path_param| {
if let Some(value) = &path_param.value {
url.replace(format!("{{{}}}", path_param.name).as_str(), value.as_str())
} else {
url
}
})
}
fn reqeust(&self, request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
let query_params = self
.query_parameters()
.filter_map(|query_param| {
let name = query_param.name.clone();
let value = query_param.value.clone();
if !query_param.required && value.is_none() {
return None;
}
Some((name, value.unwrap()))
})
.collect::<Vec<_>>();
let header_params = self
.header_parameters()
.filter_map(|header_param| {
let name = header_param.name.as_str();
let value = header_param.value.as_deref().unwrap_or_default();
HeaderName::from_str(name)
.ok()
.and_then(|header_name| HeaderValue::from_str(value).ok().map(|header_value| (header_name, header_value)))
})
.collect::<HeaderMap<_>>();
request.query(&query_params).headers(header_params)
}
}
impl Pane for ParameterEditor {
fn init(&mut self, state: &State) -> Result<()> {
self.init_parameters(state)?;
Ok(())
}
fn height_constraint(&self) -> Constraint {
Constraint::Fill(1)
}
fn handle_key_events(&mut self, key: KeyEvent, state: &mut State) -> Result<Option<EventResponse<Action>>> {
match state.input_mode {
InputMode::Insert => match key.code {
KeyCode::Enter => Ok(Some(EventResponse::Stop(Action::Submit))),
_ => {
self.input.handle_event(&Event::Key(key));
Ok(Some(EventResponse::Stop(Action::Noop)))
},
},
_ => Ok(None),
}
}
fn update(&mut self, action: Action, state: &mut State) -> Result<Option<Action>> {
match action {
Action::Update => {},
Action::Down => {
if let Some(parameters) = self.parameters.get_mut(self.selected_parameter).as_mut() {
let i = match parameters.table_state.selected() {
Some(i) => {
if i >= parameters.items.len().saturating_sub(1) {
0
} else {
i + 1
}
},
None => 0,
};
parameters.table_state.select(Some(i));
}
},
Action::Up => {
if let Some(parameters) = self.parameters.get_mut(self.selected_parameter).as_mut() {
let i = match parameters.table_state.selected() {
Some(i) => {
if i == 0 {
parameters.items.len().saturating_sub(1)
} else {
i - 1
}
},
None => 0,
};
parameters.table_state.select(Some(i));
}
},
Action::Tab(index) if index < self.parameters.len().try_into()? => {
self.selected_parameter = index as usize;
},
Action::TabNext => {
let next_tab_index = self.selected_parameter + 1;
self.selected_parameter = if next_tab_index < self.parameters.len() { next_tab_index } else { 0 };
},
Action::TabPrev => {
self.selected_parameter =
if self.selected_parameter > 0 { self.selected_parameter - 1 } else { self.parameters.len() - 1 };
},
Action::Focus => {
self.focused = true;
},
Action::UnFocus => {
self.focused = false;
},
Action::Submit if state.input_mode == InputMode::Normal && !self.parameters.is_empty() => {
state.input_mode = InputMode::Insert;
if let Some(parameter) = self
.parameters
.get(self.selected_parameter)
.and_then(|parameters| parameters.table_state.selected().and_then(|i| parameters.items.get(i)))
{
self.input = self.input.clone().with_value(parameter.value.clone().unwrap_or_default());
}
},
Action::Submit if state.input_mode == InputMode::Insert && !self.parameters.is_empty() => {
state.input_mode = InputMode::Normal;
if let Some(parameter) = self
.parameters
.get_mut(self.selected_parameter)
.and_then(|parameters| parameters.table_state.selected().and_then(|i| parameters.items.get_mut(i)))
{
if !self.input.value().is_empty() {
parameter.value = Some(self.input.value().to_string());
} else {
parameter.value = None;
}
}
self.input.reset();
},
Action::AddHeader(header_name) => {
if let Some(param_tab) = self.parameters.iter_mut().find(|item| item.location.to_lowercase().eq("header")) {
param_tab.items.push(ParameterItem { name: header_name, ..Default::default() });
}
},
Action::RemoveHeader(header_name) => {
if let Some(param_tab) = self.parameters.iter_mut().find(|item| item.location.to_lowercase().eq("header")) {
if let Some(last_header_index) = param_tab
.items
.iter()
.enumerate()
.filter_map(|(index, item)| if item.name.eq(&header_name) { Some(index) } else { None })
.last()
{
param_tab.items.remove(last_header_index);
}
}
},
Action::AddQuery(query_name) => {
if let Some(param_tab) = self.parameters.iter_mut().find(|item| item.location.to_lowercase().eq("query")) {
param_tab.items.push(ParameterItem { name: query_name, ..Default::default() });
}
},
Action::RemoveQuery(query_name) => {
if let Some(param_tab) = self.parameters.iter_mut().find(|item| item.location.to_lowercase().eq("query")) {
if let Some(last_query_index) = param_tab
.items
.iter()
.enumerate()
.filter_map(|(index, item)| if item.name.eq(&query_name) { Some(index) } else { None })
.last()
{
param_tab.items.remove(last_query_index);
}
}
},
_ => {},
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, state: &State) -> Result<()> {
let inner = area.inner(Margin { horizontal: 1, vertical: 1 });
frame.render_widget(
Tabs::new(self.parameters.iter().map(|item| {
Span::styled(item.location.clone(), Style::default().fg(self.location_color(item.location.as_str()))).dim()
}))
.divider(symbols::DOT)
.highlight_style(Style::default().add_modifier(Modifier::BOLD | Modifier::UNDERLINED).not_dim())
.select(self.selected_parameter),
inner,
);
let inner = Rect { x: inner.x, y: inner.y + 1, width: inner.width, height: inner.height - 1 };
if let Some(parameters) = self.parameters.get_mut(self.selected_parameter) {
let selected = parameters.table_state.selected().unwrap_or(0);
let rows = parameters.items.iter().enumerate().map(|(index, item)| {
let required = match item.required {
true => " * ",
false => " ",
};
let value = match &item.value {
Some(value) => Span::from(value),
None => Span::styled(String::from("No Value"), Style::default().dim()),
};
let value = match state.input_mode {
InputMode::Insert if selected == index && self.focused => Span::default(),
_ => value,
};
Row::new(vec![
Cell::from(Line::from(vec![Span::from(required).style(Color::Red), Span::from(item.name.clone())])),
Cell::from(Line::from(vec![Span::from(symbols::line::VERTICAL), value])),
])
});
let row_widths = [Constraint::Fill(1), Constraint::Fill(2)];
let column_widths = Layout::horizontal(row_widths).split(inner);
if !parameters.items.is_empty() {
let table = Table::new(rows, vec![column_widths[0].width, column_widths[1].width])
.highlight_symbol(symbols::scrollbar::HORIZONTAL.end)
.highlight_spacing(HighlightSpacing::Always)
.row_highlight_style(Style::default().add_modifier(Modifier::BOLD));
frame.render_stateful_widget(table, inner, &mut parameters.table_state);
} else {
let location = parameters.location.to_lowercase();
let empty_msg = if location.eq("query") || location.eq("header") {
format!(" No {location} item available. try [{location} add {location}-name] command to add one.")
} else {
format!(" No {location} item available.")
};
frame.render_widget(Paragraph::new(empty_msg).style(Style::default().dim()), inner);
}
if self.focused && InputMode::Insert == state.input_mode {
let input_area = Rect {
x: inner.x + column_widths[0].width + 3,
y: inner.y + selected.saturating_sub(parameters.table_state.offset()) as u16,
width: column_widths[1].width - 3,
height: 1,
};
let scroll = self.input.visual_scroll(input_area.width as usize);
let input =
Paragraph::new(Line::from(vec![
Span::styled(self.input.value(), Style::default().fg(Color::LightBlue)).not_dim()
]))
.scroll((0, scroll as u16));
frame.set_cursor_position(Position::new(
input_area.x + self.input.visual_cursor().saturating_sub(scroll) as u16,
input_area.y,
));
frame.render_widget(input, input_area);
}
}
frame.render_widget(
Block::default()
.title("Parameters")
.borders(Borders::ALL)
.border_style(self.border_style())
.border_type(self.border_type()),
area,
);
Ok(())
}
}