Skip to content

Commit

Permalink
Merge pull request #27 from zaghaghi/feat/reques-page-commands
Browse files Browse the repository at this point in the history
Added support for query/header commands
  • Loading branch information
zaghaghi authored May 5, 2024
2 parents f69a8fa + 5b1d933 commit 98831b1
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 42 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,33 @@ jobs:

CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

publish-docker-hub:
name: Publishing to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: zaghaghi/openapi-tui
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
provenance: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,11 @@ Then, add `openapi-tui` to your `configuration.nix`
| Command | Description |
|:--------|:------------|
| `q` | Quit |
| `send`, `s` | send request|

| `send`, `s` | Send request |
| `query`, `q` | Add or remove query strings. sub-commands are `add` or `rm`. e.g. `query add page` |
| `header`, `h` | Add or remove headers. sub-commands are `add` or `rm`. e.g. `header add x-api-key` |
| `request`, `r` | Load request payload. e.g. `request open /home/hamed/payload.json` |
| `response`, `s` | Save response payload e.g/ `response save /home/hamed/result.json` |


# Implemented Features
Expand All @@ -189,15 +192,14 @@ Then, add `openapi-tui` to your `configuration.nix`
- [X] Refactor footer, add flash footer messages

# Next Release
- [ ] Import request body file
- [ ] Save response body and header
- [X] Import request body file
- [X] Save response body and header
- [X] Command history with ↑/↓
- [X] Support array query strings
- [X] Suppert extra headers

# Backlog
- [ ] Schema Types (openapi-31)
- [ ] Display Key Mappings in Popup
- [ ] Cache Schema Styles
- [ ] Read Spec from STDIN
- [ ] Support array query strings
- [ ] Suppert extra headers
- [ ] Request progress bar
6 changes: 6 additions & 0 deletions src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,10 @@ pub enum Action {
Dial,
History,
CloseHistory,
AddQuery(String),
RemoveQuery(String),
AddHeader(String),
RemoveHeader(String),
OpenRequestPayload(String),
SaveResponsePayload(String),
}
64 changes: 58 additions & 6 deletions src/pages/phone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,57 @@ impl Phone {

Ok(request_builder.build()?)
}

fn handle_commands(&self, command_args: String) -> Option<Action> {
if command_args.eq("q") {
return Some(Action::Quit);
}
if command_args.eq("send") || command_args.eq("s") {
return Some(Action::Dial);
}
if command_args.starts_with("query ") || command_args.starts_with("q ") {
let command_parts = command_args.split(' ').filter(|item| !item.is_empty()).collect::<Vec<_>>();
if command_parts.len() == 3 {
if command_parts[1].eq("add") {
return Some(Action::AddQuery(command_parts[2].into()));
}
if command_parts[1].eq("rm") {
return Some(Action::RemoveQuery(command_parts[2].into()));
}
}
return Some(Action::TimedStatusLine("invalid query args. query add/rm <query-name>".into(), 3));
}
if command_args.starts_with("header ") || command_args.starts_with("h ") {
let command_parts = command_args.split(' ').filter(|item| !item.is_empty()).collect::<Vec<_>>();
if command_parts.len() == 3 {
if command_parts[1].eq("add") {
return Some(Action::AddHeader(command_parts[2].into()));
}
if command_parts[1].eq("rm") {
return Some(Action::RemoveHeader(command_parts[2].into()));
}
}
return Some(Action::TimedStatusLine("invalid header args. header add/rm <query-name>".into(), 3));
}
if command_args.starts_with("request ") || command_args.starts_with("r ") {
let command_parts = command_args.split(' ').filter(|item| !item.is_empty()).collect::<Vec<_>>();
if command_parts.len() == 3 && command_parts[1].eq("open") {
return Some(Action::OpenRequestPayload(command_parts[2].into()));
}
return Some(Action::TimedStatusLine("invalid request args. request open <payload-file-name>".into(), 3));
}
if command_args.starts_with("response ") || command_args.starts_with("s ") {
let command_parts = command_args.split(' ').filter(|item| !item.is_empty()).collect::<Vec<_>>();
if command_parts.len() == 3 && command_parts[1].eq("save") {
return Some(Action::SaveResponsePayload(command_parts[2].into()));
}
return Some(Action::TimedStatusLine("invalid response args. response save <payload-file-name>".into(), 3));
}
Some(Action::TimedStatusLine(
"unknown command. available commands are: send, query, header, request, response".into(),
3,
))
}
}

impl Page for Phone {
Expand Down Expand Up @@ -206,12 +257,13 @@ impl Page for Phone {
if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
pane.update(Action::Focus, state)?;
}
if args.eq("q") {
actions.push(Some(Action::Quit));
} else if args.eq("send") || args.eq("s") {
actions.push(Some(Action::Dial));
} else {
actions.push(Some(Action::TimedStatusLine("unknown command".into(), 1)));
if let Some(action) = self.handle_commands(args) {
for pane in self.panes.iter_mut() {
actions.push(pane.update(action.clone(), state)?);
}
if let Action::TimedStatusLine(_, _) = action {
actions.push(Some(action))
}
}
},
Action::FooterResult(_cmd, None) => {
Expand Down
27 changes: 25 additions & 2 deletions src/panes/body_editor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::sync::Arc;
use std::{io::Read, sync::Arc};

use color_eyre::eyre::Result;
use crossterm::event::{KeyCode, KeyEvent};
Expand Down Expand Up @@ -135,6 +135,19 @@ impl Pane for BodyEditor<'_> {
Action::UnFocus => {
self.focused = false;
},
Action::OpenRequestPayload(filepath) => {
if let Err(error) = std::fs::File::open(filepath)
.and_then(|mut file| {
let mut buffer = String::new();
file.read_to_string(&mut buffer).map(|_| buffer)
})
.map(|item| {
self.input = TextArea::from(item.lines());
})
{
return Ok(Some(Action::TimedStatusLine(format!("can't open or read file content: {error}"), 5)));
}
},
_ => {},
}
Ok(None)
Expand All @@ -151,7 +164,17 @@ impl Pane for BodyEditor<'_> {
}

if !self.content_types.is_empty() {
frame.render_widget(self.input.widget(), inner);
if !self.input.is_empty() || state.input_mode == InputMode::Insert {
frame.render_widget(self.input.widget(), inner);
} else {
frame.render_widget(
Paragraph::new(
" Press enter to start editing,\n or try [request open payload-file-path] command to load from a file.",
)
.style(Style::default().dim()),
inner,
);
}
}

let content_types = if !self.content_types.is_empty() {
Expand Down
11 changes: 7 additions & 4 deletions src/panes/footer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,13 @@ impl Pane for FooterPane {
self.input.handle_event(&Event::Key(key));
let response = match key.code {
KeyCode::Enter => {
self.command_history.push_front(self.input.to_string());
self.command_history.truncate(CONFIG.max_command_history);
self.command_history_index = None;
Some(EventResponse::Stop(Action::FooterResult(self.command.clone(), Some(self.input.to_string()))))
let command = self.input.to_string();
if !command.is_empty() {
self.command_history.push_front(self.input.to_string());
self.command_history.truncate(CONFIG.max_command_history);
self.command_history_index = None;
}
Some(EventResponse::Stop(Action::FooterResult(self.command.clone(), Some(command))))
},
KeyCode::Esc => {
self.command_history_index = None;
Expand Down
84 changes: 63 additions & 21 deletions src/panes/parameter_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,16 @@ impl ParameterEditor {
table_state: TableState::default().with_selected(0),
});
}
if !query_items.is_empty() {
self.parameters.push(ParameterTab {
location: "Query".to_string(),
items: query_items,
table_state: TableState::default().with_selected(0),
});
}
if !header_items.is_empty() {
self.parameters.push(ParameterTab {
location: "Header".to_string(),
items: header_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(),
Expand Down Expand Up @@ -242,7 +238,7 @@ impl Pane for ParameterEditor {
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() - 1 {
if i >= parameters.items.len().saturating_sub(1) {
0
} else {
i + 1
Expand All @@ -258,7 +254,7 @@ impl Pane for ParameterEditor {
let i = match parameters.table_state.selected() {
Some(i) => {
if i == 0 {
parameters.items.len() - 1
parameters.items.len().saturating_sub(1)
} else {
i - 1
}
Expand Down Expand Up @@ -311,6 +307,42 @@ impl Pane for ParameterEditor {
}
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)
Expand Down Expand Up @@ -355,12 +387,22 @@ impl Pane for ParameterEditor {
});
let row_widths = [Constraint::Fill(1), Constraint::Fill(2)];
let column_widths = Layout::horizontal(row_widths).split(inner);
let table = Table::new(rows, vec![column_widths[0].width, column_widths[1].width])
.highlight_symbol(symbols::scrollbar::HORIZONTAL.end)
.highlight_spacing(HighlightSpacing::Always)
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
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)
.highlight_style(Style::default().add_modifier(Modifier::BOLD));

frame.render_stateful_widget(table, inner, &mut parameters.table_state);
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 {
Expand Down
22 changes: 20 additions & 2 deletions src/panes/response_viewer.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::sync::Arc;
use std::{io::Write, sync::Arc};

use color_eyre::eyre::Result;
use crossterm::event::KeyEvent;
Expand Down Expand Up @@ -87,7 +87,7 @@ impl Pane for ResponseViewer {
}
}

fn update(&mut self, action: Action, _state: &mut State) -> Result<Option<Action>> {
fn update(&mut self, action: Action, state: &mut State) -> Result<Option<Action>> {
match action {
Action::Update => {},
Action::Submit => return Ok(Some(Action::Dial)),
Expand All @@ -109,6 +109,19 @@ impl Pane for ResponseViewer {
Action::UnFocus => {
self.focused = false;
},
Action::SaveResponsePayload(filepath) => {
if let Some(response) =
self.operation_item.operation.operation_id.as_ref().and_then(|operation_id| state.responses.get(operation_id))
{
if let Err(error) =
std::fs::File::create(filepath).and_then(|mut file| file.write_all(response.body.as_bytes()))
{
return Ok(Some(Action::TimedStatusLine(format!("can't create or write file content: {error}"), 5)));
}
} else {
return Ok(Some(Action::TimedStatusLine("response is not available".into(), 5)));
}
},
_ => {},
}
Ok(None)
Expand Down Expand Up @@ -152,6 +165,11 @@ impl Pane for ResponseViewer {
),
inner_panes[1],
);
} else {
frame.render_widget(
Paragraph::new(" No response is available. Press enter or try [send] command.").style(Style::default().dim()),
inner,
)
}

let content_types = if !self.content_types.is_empty() {
Expand Down

0 comments on commit 98831b1

Please sign in to comment.