Skip to content

Commit

Permalink
Merge pull request #36 from zaghaghi/35-set-server-url-in-command-lin…
Browse files Browse the repository at this point in the history
…e-params-or-choose-from-server-urls-section-in-openapi-specification

35 set server url in command line params or choose from server urls section in openapi specification
  • Loading branch information
zaghaghi authored Oct 4, 2024
2 parents f56c596 + 1f41e6f commit bd434be
Show file tree
Hide file tree
Showing 13 changed files with 500 additions and 395 deletions.
687 changes: 337 additions & 350 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ clap = { version = "4.5.4", features = [
] }
color-eyre = "0.6.3"
config = "0.14.0"
crossterm = { version = "0.27.0", features = ["serde", "event-stream"] }
crossterm = { version = "0.28.1", features = ["serde", "event-stream"] }
derive_deref = "1.1.1"
directories = "5.0.1"
futures = "0.3.30"
Expand All @@ -36,23 +36,23 @@ libc = "0.2.153"
log = "0.4.21"
openapi-31 = { version = "0.4.0" }
pretty_assertions = "1.4.0"
ratatui = { version = "0.27.0", features = ["serde", "macros"] }
ratatui = { version = "0.28.1", features = ["serde", "macros"] }
reqwest = { version = "0.12.2", features = ["native-tls-vendored"] }
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.115"
serde_yaml = "0.9.34-deprecated"
serde_yaml = "0.9.34+deprecated"
signal-hook = "0.3.17"
strip-ansi-escapes = "0.2.0"
strum = { version = "0.26.2", features = ["derive"] }
syntect = "5.2.0"
syntect-tui = "3.0.3"
syntect-tui = "3.0.4"
tokio = { version = "1.37.0", features = ["full"] }
tokio-util = "0.7.10"
tracing = "0.1.40"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "serde"] }
tui-input = "0.9.0"
tui-textarea = "0.5.1"
tui-input = "0.10.1"
tui-textarea = "0.6.1"

[build-dependencies]
anyhow = "1.0.86"
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ Options:
## Call Endpoints
![call](static/call.gif)

## Multiple Server Support
![call](static/switch-server.gif)

</details>

<br />
Expand Down Expand Up @@ -169,6 +172,11 @@ Then, add `openapi-tui` to your `configuration.nix`
| `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` |

# Environment Variables
| Variable | Description |
|:---------|:------------|
| `OPENAPI_TUI_DEFAULT_SERVER` | Add a custom server url to the list of servers|


# Implemented Features
- [X] Viewer
Expand All @@ -195,6 +203,7 @@ Then, add `openapi-tui` to your `configuration.nix`
- [X] Command history with ↑/↓
- [X] Support array query strings
- [X] Suppert extra headers
- [X] Support multiple servers

# Backlog
- [ ] Schema Types (openapi-31)
Expand Down
15 changes: 15 additions & 0 deletions examples/petstore.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@
"servers": [
{
"url": "https://petstore3.swagger.io/api/v3"
},
{
"url": "https://petstore3.swagger.local:{port}/api/{version}",
"variables": {
"port": {
"default": "80",
"enum": [
"80",
"8080"
]
},
"version": {
"default": "v3"
}
}
}
],
"tags": [
Expand Down
6 changes: 3 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ impl App {
self.pages[0].unfocus()?;
self.pages.insert(0, page);
self.pages[0].focus()?;
} else if let Ok(mut page) = Phone::new(operation_item.clone(), request_tx.clone()) {
} else if let Ok(mut page) = Phone::new(operation_item.clone(), request_tx.clone(), &self.state) {
self.pages[0].unfocus()?;
page.init(&self.state)?;
page.register_action_handler(action_tx.clone())?;
Expand Down Expand Up @@ -296,7 +296,7 @@ impl App {

fn draw(&mut self, frame: &mut tui::Frame<'_>) -> Result<()> {
let vertical_layout =
Layout::vertical(vec![Constraint::Max(1), Constraint::Fill(1), Constraint::Max(1)]).split(frame.size());
Layout::vertical(vec![Constraint::Max(1), Constraint::Fill(1), Constraint::Max(1)]).split(frame.area());

self.header.draw(frame, vertical_layout[0], &self.state)?;

Expand All @@ -306,7 +306,7 @@ impl App {

if let Some(popup) = &mut self.popup {
let popup_vertical_layout =
Layout::vertical(vec![Constraint::Fill(1), popup.height_constraint(), Constraint::Fill(1)]).split(frame.size());
Layout::vertical(vec![Constraint::Fill(1), popup.height_constraint(), Constraint::Fill(1)]).split(frame.area());
let popup_layout = Layout::horizontal(vec![Constraint::Fill(1), Constraint::Fill(1), Constraint::Fill(1)])
.split(popup_vertical_layout[1]);
popup.draw(frame, popup_layout[1], &self.state)?;
Expand Down
43 changes: 25 additions & 18 deletions src/pages/phone.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::sync::Arc;
use std::{collections::VecDeque, sync::Arc};

use color_eyre::eyre::Result;
use color_eyre::eyre::{ContextCompat, Result};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
prelude::*,
Expand All @@ -27,6 +27,7 @@ pub struct Phone {
focused_pane_index: usize,
panes: Vec<Box<dyn RequestPane>>,
fullscreen_pane_index: Option<usize>,
base_urls: VecDeque<String>,
}

pub trait RequestBuilder {
Expand All @@ -42,12 +43,13 @@ pub trait RequestBuilder {
pub trait RequestPane: Pane + RequestBuilder {}

impl Phone {
pub fn new(operation_item: OperationItem, request_tx: UnboundedSender<Request>) -> Result<Self> {
pub fn new(operation_item: OperationItem, request_tx: UnboundedSender<Request>, state: &State) -> Result<Self> {
let focused_border_style = Style::default().fg(Color::LightGreen);
let operation_item = Arc::new(operation_item);
let parameter_editor = ParameterEditor::new(operation_item.clone(), true, focused_border_style);
let body_editor = BodyEditor::new(operation_item.clone(), false, focused_border_style);
let response_viewer = ResponseViewer::new(operation_item.clone(), false, focused_border_style);
let base_urls = Phone::default_base_urls(&operation_item, state);
Ok(Self {
operation_item,
command_tx: None,
Expand All @@ -56,6 +58,7 @@ impl Phone {
panes: vec![Box::new(parameter_editor), Box::new(body_editor), Box::new(response_viewer)],
focused_pane_index: 0,
fullscreen_pane_index: None,
base_urls,
})
}

Expand All @@ -69,21 +72,13 @@ impl Phone {
}
}

fn base_url(&self, state: &State) -> String {
if let Some(server) = state.openapi_spec.servers.as_ref().map(|v| v.first()).unwrap_or(None) {
String::from(server.url.trim_end_matches('/'))
} else if let Some(server) = &self.operation_item.operation.servers.as_ref().map(|v| v.first()).unwrap_or(None) {
String::from(server.url.trim_end_matches('/'))
} else {
String::from("http://localhost")
}
fn default_base_urls(operation_item: &OperationItem, state: &State) -> VecDeque<String> {
state.default_server_urls(&operation_item.operation.servers).into()
}

fn build_request(&self, state: &State) -> Result<reqwest::Request> {
let url = self
.panes
.iter()
.fold(format!("{}{}", self.base_url(state), self.operation_item.path), |url, pane| pane.path(url));
fn build_request(&self) -> Result<reqwest::Request> {
let base_url = self.base_urls.front().context("no base url found")?;
let url = self.panes.iter().fold(format!("{}{}", base_url, self.operation_item.path), |url, pane| pane.path(url));
let method = reqwest::Method::from_bytes(self.operation_item.method.as_bytes())?;
let request_builder = self
.panes
Expand Down Expand Up @@ -232,6 +227,16 @@ impl Page for Phone {
actions.push(pane.update(Action::Focus, state)?);
}
},
Action::Up => {
if let Some(front) = self.base_urls.pop_front() {
self.base_urls.push_back(front);
}
},
Action::Down => {
if let Some(back) = self.base_urls.pop_back() {
self.base_urls.push_front(back);
}
},
Action::ToggleFullScreen => {
self.fullscreen_pane_index = self.fullscreen_pane_index.map_or(Some(self.focused_pane_index), |_| None);
},
Expand All @@ -243,7 +248,7 @@ impl Page for Phone {
Action::Dial => {
if let Some(request_tx) = &self.request_tx {
request_tx.send(Request {
request: self.build_request(state)?,
request: self.build_request()?,
operation_id: self.operation_item.operation.operation_id.clone().unwrap_or_default(),
})?;
}
Expand Down Expand Up @@ -286,6 +291,8 @@ impl Page for Phone {
}

fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, state: &State) -> Result<()> {
let base_url = self.base_urls.front().context("no base url found")?;

let outer_layout =
Layout::vertical(vec![Constraint::Max(3), self.panes[1].height_constraint(), self.panes[2].height_constraint()])
.split(area);
Expand All @@ -295,7 +302,7 @@ impl Page for Phone {
format!(" {} ", self.operation_item.method.as_str()),
Style::default().fg(Self::method_color(self.operation_item.method.as_str())),
),
Span::styled(self.base_url(state), Style::default().fg(Color::DarkGray)),
Span::styled(base_url, Style::default().fg(Color::DarkGray)),
Span::styled(&self.operation_item.path, Style::default().fg(Color::White)),
]))
.block(
Expand Down
29 changes: 21 additions & 8 deletions src/panes/address.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::VecDeque;

use color_eyre::eyre::Result;
use ratatui::{
prelude::*,
Expand All @@ -15,11 +17,12 @@ use crate::{
pub struct AddressPane {
focused: bool,
focused_border_style: Style,
base_urls: VecDeque<String>,
}

impl AddressPane {
pub fn new(focused: bool, focused_border_style: Style) -> Self {
Self { focused, focused_border_style }
Self { focused, focused_border_style, base_urls: VecDeque::new() }
}

fn border_style(&self) -> Style {
Expand Down Expand Up @@ -51,6 +54,11 @@ impl Pane for AddressPane {
Constraint::Max(3)
}

fn init(&mut self, state: &State) -> Result<()> {
self.base_urls = state.default_server_urls(&None).into();
Ok(())
}

fn update(&mut self, action: Action, _state: &mut State) -> Result<Option<Action>> {
match action {
Action::Focus => {
Expand All @@ -61,22 +69,27 @@ impl Pane for AddressPane {
Action::UnFocus => {
self.focused = false;
},
Action::Up => {
if let Some(front) = self.base_urls.pop_front() {
self.base_urls.push_back(front.to_string());
}
},
Action::Down => {
if let Some(back) = self.base_urls.pop_back() {
self.base_urls.push_front(back.to_string());
}
},
Action::Update => {},
Action::Submit => {},

_ => {},
}
Ok(None)
}

fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, state: &State) -> Result<()> {
if let Some(operation_item) = state.active_operation() {
let base_url = if let Some(server) = state.openapi_spec.servers.as_ref().map(|v| v.first()).unwrap_or(None) {
String::from(server.url.trim_end_matches('/'))
} else if let Some(server) = operation_item.operation.servers.as_ref().map(|v| v.first()).unwrap_or(None) {
String::from(server.url.trim_end_matches('/'))
} else {
String::from("http://localhost")
};
let base_url = self.base_urls.front().cloned().unwrap_or(String::new());
let title = operation_item.operation.summary.clone().unwrap_or_default();

let inner = area.inner(Margin { horizontal: 1, vertical: 1 });
Expand Down
2 changes: 1 addition & 1 deletion src/panes/body_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ impl Pane for BodyEditor<'_> {

if !self.content_types.is_empty() {
if !self.input.is_empty() || state.input_mode == InputMode::Insert {
frame.render_widget(self.input.widget(), inner);
frame.render_widget(&self.input, inner);
} else {
frame.render_widget(
Paragraph::new(
Expand Down
4 changes: 2 additions & 2 deletions src/panes/footer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,10 @@ impl Pane for FooterPane {
.scroll((0, scroll as u16));
frame.render_widget(input, area);

frame.set_cursor(
frame.set_cursor_position(Position::new(
area.x + ((self.input.visual_cursor()).max(scroll) - scroll) as u16 + self.command.len() as u16,
area.y + 1,
)
))
} else {
frame.render_widget(
Line::from(vec![Span::styled(self.get_status_line(), Style::default())])
Expand Down
10 changes: 5 additions & 5 deletions src/panes/parameter_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ 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::{block::*, *},
};
use ratatui::{prelude::*, widgets::*};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use tui_input::{backend::crossterm::EventHandler, Input};

Expand Down Expand Up @@ -420,7 +417,10 @@ impl Pane for ParameterEditor {
Span::styled(self.input.value(), Style::default().fg(Color::LightBlue)).not_dim()
]))
.scroll((0, scroll as u16));
frame.set_cursor(input_area.x + self.input.visual_cursor().saturating_sub(scroll) as u16, input_area.y);
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);
}
}
Expand Down
34 changes: 32 additions & 2 deletions src/state.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::{collections::HashMap, env};

use color_eyre::eyre::Result;
use openapi_31::v31::{Openapi, Operation};
use openapi_31::v31::{Openapi, Operation, Server};

use crate::response::Response;

Expand Down Expand Up @@ -143,6 +143,36 @@ impl State {
.count()
}
}

fn default_url(server: &Server) -> String {
let mut url = server.url.clone();
if let Some(variables) = &server.variables {
for (k, v) in variables {
url = url.replace(format!("{{{}}}", k).as_str(), &v.default);
}
}
url.trim_end_matches('/').to_string()
}

pub fn default_server_urls(&self, extra_servers: &Option<Vec<Server>>) -> Vec<String> {
let mut result = vec![];
if let Ok(url) = env::var("OPENAPI_TUI_DEFAULT_SERVER") {
result.push(url.trim_end_matches('/').to_string());
}

extra_servers.iter().flatten().for_each(|server| {
result.push(State::default_url(server));
});

self.openapi_spec.servers.iter().flatten().for_each(|server| {
result.push(State::default_url(server));
});

if result.is_empty() {
result.push("http://localhost".to_string());
}
result
}
}

impl OperationItem {
Expand Down
Binary file added static/switch-server.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit bd434be

Please sign in to comment.