Skip to content

Commit

Permalink
feat: handling other http methods
Browse files Browse the repository at this point in the history
  • Loading branch information
wllfaria committed May 23, 2024
1 parent a9ec9e7 commit bbd57e8
Show file tree
Hide file tree
Showing 32 changed files with 331 additions and 114 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# ✅ TODO

## Core
- [x] most used http methods (GET, PUT, POST, PATCH, DELETE)
- [ ] add environments for collections, to define variables to replace keys
- [ ] allow for authentication, primarily Bearer tokens
- [ ] import collections from postman
- [ ] support HTML, XML, plain text and other response types
- [ ] support other types of bodies, Multipart, URL encoded forms


## Other
- [ ] prevent from synchronizing to disk when no changes were made
- [ ] force synchronization when switching from dirty requests
- [ ] allow for saving a sample response from a request
- [ ] moving requests from folders to others
- [ ] creating requests inside of folders
- [ ] better CLI interfacing
- [ ] edit bodies in $EDITOR
- [ ] add scripting to requests
Expand Down
1 change: 1 addition & 0 deletions reqtui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ tree-sitter-json.workspace = true
lazy_static.workspace = true

ropey = "1.6.1"
async-trait = "0.1.80"

[build-dependencies]
cc="*"
8 changes: 7 additions & 1 deletion reqtui/src/collection/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,13 @@ pub struct Request {
pub uri: String,
pub body: Option<String>,
#[serde(rename = "bodyType")]
pub body_type: Option<String>,
pub body_type: Option<BodyType>,
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub enum BodyType {
#[serde(rename = "json")]
Json,
}

impl Hash for Request {
Expand Down
3 changes: 3 additions & 0 deletions reqtui/src/net.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
pub mod request_manager;
pub mod request_strategies;
pub mod response_decoders;

pub use request_manager::handle_request;
95 changes: 52 additions & 43 deletions reqtui/src/net/request_manager.rs
Original file line number Diff line number Diff line change
@@ -1,65 +1,74 @@
use crate::{
collection::types::Request,
collection::types::{BodyType, Request},
net::request_strategies::{http_strategy::HttpResponse, RequestStrategy},
text_object::{Readonly, TextObject},
};
use reqwest::header::{HeaderMap, HeaderValue};
use std::time::Duration;
use tokio::sync::mpsc::UnboundedSender;

#[derive(Debug, PartialEq)]
pub struct ReqtuiResponse {
pub struct Response {
pub body: String,
pub pretty_body: TextObject<Readonly>,
pub headers: HeaderMap<HeaderValue>,
pub duration: Duration,
}

#[derive(Debug, PartialEq)]
pub enum ReqtuiNetRequest {
Request(Request),
Response(ReqtuiResponse),
Error(String),
pub struct RequestManager;

impl RequestManager {
pub async fn handle<S>(strategy: S, request: Request) -> anyhow::Result<Response>
where
S: RequestStrategy,
{
let response = strategy.handle(request).await?;
Ok(response)
}
}

pub enum ContentType {
TextPlain,
TextHtml,
TextCss,
TextJavascript,
ApplicationJson,
ApplicationXml,
}

impl From<&str> for ContentType {
fn from(value: &str) -> Self {
match value {
_ if value.to_ascii_lowercase().contains("application/json") => Self::ApplicationJson,
_ if value.to_ascii_lowercase().contains("application/xml") => Self::ApplicationXml,
_ if value.to_ascii_lowercase().contains("text/plain") => Self::TextPlain,
_ if value.to_ascii_lowercase().contains("text/plain") => Self::TextPlain,
_ if value.to_ascii_lowercase().contains("text/html") => Self::TextHtml,
_ if value.to_ascii_lowercase().contains("text/css") => Self::TextCss,
_ if value.to_ascii_lowercase().contains("text/javascript") => Self::TextJavascript,
_ => Self::TextPlain,
}
}
}

#[tracing::instrument(skip_all)]
pub fn handle_request(request: Request, response_tx: UnboundedSender<ReqtuiNetRequest>) {
pub fn handle_request(request: Request, response_tx: UnboundedSender<Response>) {
tracing::debug!("starting to handle user request");
tokio::spawn(async move {
let client = reqwest::Client::new();
match client.get(request.uri).send().await {
Ok(res) => {
tracing::debug!("request handled successfully, sending response");

let headers = res.headers().to_owned();

let body: serde_json::Value = res
.json()
.await
.map_err(|_| {
response_tx.send(ReqtuiNetRequest::Error(
"failed to decode json response".into(),
))
})
.expect("failed to send response through channel");

let pretty_body = serde_json::to_string_pretty(&body)
.map_err(|_| {
response_tx.send(ReqtuiNetRequest::Error(
"failed to decode json response".into(),
))
})
.expect("failed to send response through channel");

let body = body.to_string();
let pretty_body = TextObject::from(&pretty_body);
let result = match request.body_type.as_ref() {
// if we dont have a body type, this is a GET request, so we use HTTP strategy
None => RequestManager::handle(HttpResponse, request).await,
Some(body_type) => match body_type {
BodyType::Json => RequestManager::handle(HttpResponse, request).await,
},
};

response_tx
.send(ReqtuiNetRequest::Response(ReqtuiResponse {
body,
pretty_body,
headers,
}))
.expect("failed to send response through channel");
match result {
Ok(response) => response_tx.send(response).ok(),
Err(e) => {
tracing::error!("{e:?}");
todo!();
}
Err(_) => todo!(),
}
});
}
8 changes: 8 additions & 0 deletions reqtui/src/net/request_strategies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pub mod http_strategy;

use crate::{collection::types::Request, net::request_manager::Response};

#[async_trait::async_trait]
pub trait RequestStrategy {
async fn handle(&self, request: Request) -> anyhow::Result<Response>;
}
122 changes: 122 additions & 0 deletions reqtui/src/net/request_strategies/http_strategy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use crate::{
collection::types::{Request, RequestMethod},
net::{
request_manager::Response,
request_strategies::RequestStrategy,
response_decoders::{decoder_from_headers, ResponseDecoder},
},
};

pub struct HttpResponse;

#[async_trait::async_trait]
impl RequestStrategy for HttpResponse {
async fn handle(&self, request: Request) -> anyhow::Result<Response> {
let client = reqwest::Client::new();

match request.method {
RequestMethod::Get => self.handle_get_request(client, request).await,
RequestMethod::Post => self.handle_post_request(client, request).await,
RequestMethod::Put => self.handle_put_request(client, request).await,
RequestMethod::Patch => self.handle_patch_request(client, request).await,
RequestMethod::Delete => self.handle_delete_request(client, request).await,
}
}
}

impl HttpResponse {
async fn handle_get_request(
&self,
client: reqwest::Client,
request: Request,
) -> anyhow::Result<Response> {
let now = std::time::Instant::now();
match client.get(request.uri).send().await {
Ok(response) => {
let decoder = decoder_from_headers(response.headers());
decoder.decode(response, now).await
}
Err(_) => todo!(),
}
}

async fn handle_post_request(
&self,
client: reqwest::Client,
request: Request,
) -> anyhow::Result<Response> {
let now = std::time::Instant::now();
match client
.post(request.uri)
.json(&request.body.unwrap_or_default())
.send()
.await
{
Ok(response) => {
let decoder = decoder_from_headers(response.headers());
decoder.decode(response, now).await
}
Err(_) => todo!(),
}
}

async fn handle_put_request(
&self,
client: reqwest::Client,
request: Request,
) -> anyhow::Result<Response> {
let now = std::time::Instant::now();
match client
.put(request.uri)
.json(&request.body.unwrap_or_default())
.send()
.await
{
Ok(response) => {
let decoder = decoder_from_headers(response.headers());
decoder.decode(response, now).await
}
Err(_) => todo!(),
}
}

async fn handle_patch_request(
&self,
client: reqwest::Client,
request: Request,
) -> anyhow::Result<Response> {
let now = std::time::Instant::now();
match client
.patch(request.uri)
.json(&request.body.unwrap_or_default())
.send()
.await
{
Ok(response) => {
let decoder = decoder_from_headers(response.headers());
decoder.decode(response, now).await
}
Err(_) => todo!(),
}
}

async fn handle_delete_request(
&self,
client: reqwest::Client,
request: Request,
) -> anyhow::Result<Response> {
let now = std::time::Instant::now();
match client
.delete(request.uri)
.json(&request.body.unwrap_or_default())
.send()
.await
{
Ok(response) => {
let decoder = decoder_from_headers(response.headers());
decoder.decode(response, now).await
}
Err(_) => todo!(),
}
}
}
24 changes: 24 additions & 0 deletions reqtui/src/net/response_decoders.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
mod json_decoder;

use crate::net::{
request_manager::{ContentType, Response},
response_decoders::json_decoder::JsonDecoder,
};
use reqwest::header::HeaderMap;
use std::time::Instant;

#[async_trait::async_trait]
pub trait ResponseDecoder {
async fn decode(&self, response: reqwest::Response, start: Instant)
-> anyhow::Result<Response>;
}

pub fn decoder_from_headers(headers: &HeaderMap) -> impl ResponseDecoder {
match headers.get("Content-Type") {
Some(header) => match ContentType::from(header.to_str().unwrap_or_default()) {
ContentType::ApplicationJson => JsonDecoder,
_ => JsonDecoder,
},
None => JsonDecoder,
}
}
30 changes: 30 additions & 0 deletions reqtui/src/net/response_decoders/json_decoder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use crate::{
net::{request_manager::Response, response_decoders::ResponseDecoder},
text_object::TextObject,
};
use std::time::Instant;

pub struct JsonDecoder;

#[async_trait::async_trait]
impl ResponseDecoder for JsonDecoder {
async fn decode(
&self,
response: reqwest::Response,
start: Instant,
) -> anyhow::Result<Response> {
let duration = start.elapsed();
let headers = response.headers().to_owned();
let body: serde_json::Value = response.json().await?;
let pretty_body = serde_json::to_string_pretty(&body)?;
let pretty_body = TextObject::from(&pretty_body);
let body = body.to_string();

Ok(Response {
body,
pretty_body,
headers,
duration,
})
}
}
Loading

0 comments on commit bbd57e8

Please sign in to comment.