Skip to content

Commit

Permalink
Add functions to http mod to handle post requests
Browse files Browse the repository at this point in the history
  • Loading branch information
ge3224 committed Mar 20, 2023
1 parent decbd3e commit 5e1cc23
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 140 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "neocities_cli"
version = "0.1.1"
version = "0.1.2"
author = ["Jacob Benison <[email protected]>"]
description = "A CLI tool for managing websites hosted on Neocities."
keywords = ["neocities", "webmaster", "open-source", "static-site"]
Expand Down
141 changes: 105 additions & 36 deletions src/api/delete.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
use std::error::Error;

use reqwest::header::AUTHORIZATION;
use reqwest::Client;
use reqwest::Response;
use super::credentials::Credentials;
use super::http::post_request_body;
use super::http::HttpRequestInfo;
use crate::api::credentials::Auth;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use std::error::Error;

use super::credentials::Credentials;
use crate::api::credentials::Auth;
/// Handles the request to delete file(s) from a Neocities website using the
/// following endpoint: `/api/delete`
pub struct NcDelete {}

/// Contains data received from Neocities in response to a request to `/api/delete`
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteRequest {
pub struct DeleteResponse {
/// A status message
pub result: String,
#[serde(rename = "error_type")]
Expand All @@ -22,17 +23,11 @@ pub struct DeleteRequest {
pub message: String,
}

impl DeleteRequest {
/// Prepares and sends a request for specified files to be deleted from a Neocities user's website.
/// It awaits a response and returns either a DeleteResponse or an error.
#[tokio::main]
pub async fn fetch(
cred: Credentials,
args: Vec<String>,
) -> Result<DeleteRequest, Box<dyn Error>> {
impl NcDelete {
fn request_info(args: Vec<String>) -> Result<HttpRequestInfo, Box<dyn std::error::Error>> {
let url: String;
let api_key: Option<String>;

let cred = Credentials::new();
let auth = Auth::authenticate(cred, String::from("delete"), None);

match auth {
Expand All @@ -51,27 +46,101 @@ impl DeleteRequest {
files.push_str("filenames[]=");
files.push_str(arg);
}
let pk = HttpRequestInfo {
uri: url,
api_key,
body: Some(files),
multipart: None,
};
Ok(pk)
}

let req = Client::new();
let res: Response;

if let Some(k) = api_key {
res = req
.post(&url)
.header(AUTHORIZATION, format!("Bearer {}", k))
.body(files)
.send()
.await?;
} else {
res = req.post(&url).body(files).send().await?;
fn to_delete_response(
value: serde_json::Value,
) -> Result<DeleteResponse, Box<dyn std::error::Error>> {
let attempt = serde_json::from_value(value);
match attempt {
Ok(res) => Ok(res),
_ => {
let e: Box<dyn std::error::Error> = String::from("a problem occurred while converting the deserialized json to the DeleteResponse type").into();
return Err(e);
}
}
}

match res.status() {
reqwest::StatusCode::OK => {
let body = res.json::<DeleteRequest>().await?;
Ok(body)
}
_ => return Err(String::from("error deleting file").into()),
/// Prepares and sends a request for specified files to be deleted from a Neocities user's website.
/// It awaits a response and returns either a DeleteResponse or an error.
pub fn fetch(args: Vec<String>) -> Result<DeleteResponse, Box<dyn Error>> {
// get http path and api_key for headers
let req_info = match NcDelete::request_info(args) {
Ok(v) => v,
Err(e) => return Err(e),
};

match post_request_body(req_info.uri, req_info.api_key, req_info.body) {
Ok(res) => match NcDelete::to_delete_response(res) {
Ok(ir) => Ok(ir),
Err(e) => Err(e),
},
Err(e) => Err(e),
}
}
}

#[cfg(test)]
mod tests {
use super::DeleteResponse;
use crate::api::{credentials::ENV_KEY, delete::NcDelete};
use std::env;

#[test]
fn delete_request_path() {
let preserve_key = env::var(ENV_KEY);
env::set_var(ENV_KEY, "foo");

let mock_args = vec![String::from("foo")];
let pk = NcDelete::request_info(mock_args).unwrap();

assert_eq!(pk.api_key.unwrap(), "foo");
assert_eq!(pk.uri, "https://neocities.org/api/delete");
assert_eq!(pk.body.unwrap(), "filenames[]=foo");

// reset environment var
match preserve_key {
Ok(v) => env::set_var(ENV_KEY, v),
_ => env::remove_var(ENV_KEY),
}
}

#[test]
fn convert_value_to_delete_response() {
let mock_str_1 = r#"
{
"result": "success",
"message": "file(s) have been deleted"
}"#;

let v: serde_json::Value = serde_json::from_str(mock_str_1).unwrap();
let dr: DeleteResponse = NcDelete::to_delete_response(v).unwrap();

assert_eq!(dr.result, "success");
assert_eq!(dr.message, "file(s) have been deleted");

let mock_str_2 = r#"
{
"result": "error",
"error_type": "missing_files",
"message": "foo.html was not found on your site, canceled deleting"
}"#;

let v: serde_json::Value = serde_json::from_str(mock_str_2).unwrap();
let dr: DeleteResponse = NcDelete::to_delete_response(v).unwrap();

assert_eq!(dr.result, "error");
assert_eq!(dr.error_type.unwrap(), "missing_files");
assert_eq!(
dr.message,
"foo.html was not found on your site, canceled deleting"
);
}
}
173 changes: 146 additions & 27 deletions src/api/http.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
use reqwest::StatusCode;
use std::path::PathBuf;

use reqwest::{header::AUTHORIZATION, multipart, Body, Client, Response, StatusCode};
use tokio::fs::File;
use tokio_util::codec::{BytesCodec, FramedRead};

/// Contains specific data for forming http requests to interact with the Neocities API.
pub struct HttpRequestInfo {
/// The path in an http request-line
pub uri: String,
/// An optional Neocities API Key, which will be added to an http request's header
pub api_key: Option<String>,
/// An optional http request body, used on POST requests
pub body: Option<String>,
/// Indicates whether a request should include multipart/form-data
pub multipart: Option<Vec<String>>,
}

#[tokio::main]
/// Prepares and sends a GET request to the Neocities API. It awaits a respons and returns either a
/// response body or an error.
#[tokio::main]
pub async fn get_request(
url: String,
api_key: Option<String>,
Expand All @@ -19,30 +35,109 @@ pub async fn get_request(
res = req.get(url.as_str()).send().await?;
}

match res.status() {
StatusCode::OK => {
let raw = res.text().await?;
let status = res.status();
if let reqwest::StatusCode::OK = status {
let raw = res.text().await?;
let body: serde_json::Value = serde_json::from_str(&raw)?;
Ok(body)
} else {
Err(status_message(status).into())
}
}

let body: serde_json::Value = serde_json::from_str(&raw)?;
/// Prepares and sends a POST request to the Neocities API containing multipart/form-data.
#[tokio::main]
pub async fn post_request_multipart(
uri: String,
api_key: Option<String>,
multipart: Option<Vec<String>>,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
let client = Client::new();
let mut form = multipart::Form::new();

Ok(body)
}
StatusCode::NOT_FOUND => {
let e: Box<dyn std::error::Error> =
String::from(status_message(StatusCode::NOT_FOUND)).into();
Err(e)
}
StatusCode::BAD_GATEWAY => {
let e: Box<dyn std::error::Error> =
String::from(status_message(StatusCode::BAD_GATEWAY)).into();
Err(e)
if let Some(a) = multipart {
for arg in a.iter() {
let path = PathBuf::from(&arg);

let filepath: String;
if let Some(p) = path.to_str() {
filepath = p.to_string();
} else {
return Err(format!("problem with file/path: {arg}").into());
}

let file = File::open(path).await?;
let stream = FramedRead::new(file, BytesCodec::new());
let file_body = Body::wrap_stream(stream);

let some_file = multipart::Part::stream(file_body).file_name(filepath.clone());
form = form.part(filepath, some_file);
}
_ => {
// TODO handle other status codes
} else {
return Err(format!("no filepaths were given").into());
}

let res: Response;
if let Some(k) = api_key {
res = client
.post(&uri)
.header(AUTHORIZATION, format!("Bearer {}", k))
.multipart(form)
.send()
.await?;
} else {
res = client.post(&uri).multipart(form).send().await?;
}

let status = res.status();
if let reqwest::StatusCode::OK = status {
let raw = res.text().await?;
let body: serde_json::Value = serde_json::from_str(&raw)?;
Ok(body)
} else {
Err(status_message(status).into())
}
}

/// Prepares and sends a POST request to the Neocities API. It awaits a respons and returns either a
/// response body or an error.
#[tokio::main]
pub async fn post_request_body(
uri: String,
api_key: Option<String>,
body: Option<String>,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
// files should be listed for the two Neocities endpoints that use the POST method:
// `/api/upload/` and `/api/delete/`
let files = match body {
Some(f) => f,
None => {
let e: Box<dyn std::error::Error> =
format!("The Neocities API could not find site '{}'.", url).into();
Err(e)
String::from("not files were given for this request").into();
return Err(e);
}
};

let req = reqwest::Client::new();
let res: reqwest::Response;
if let Some(k) = api_key {
res = req
.post(&uri)
.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", k))
.body(files)
.send()
.await?;
} else {
res = req.post(&uri).body(files).send().await?;
}

let status = res.status();
if let reqwest::StatusCode::OK = status {
let raw = res.text().await?;
let body: serde_json::Value = serde_json::from_str(&raw)?;
Ok(body)
} else {
Err(status_message(status).into())
}
}

Expand All @@ -64,21 +159,45 @@ fn status_message(code: StatusCode) -> String {
let msg = "502 Bad Gateway - This error response means that the server, while working as a gateway to get a response needed to handle the request, got an invalid response.";
return String::from(msg);
}
_ => String::from(""),
_ => {
if let Some(reason) = code.canonical_reason() {
String::from(reason)
} else {
String::from(code.to_string())
}
}
}
}

#[cfg(test)]
mod tests {
use super::get_request;
use super::{get_request, post_request_body, post_request_multipart};

#[test]
fn basic_request() {
fn basic_get_request() {
let res = get_request("https://httpbin.org/ip".to_string(), None);
assert_eq!(res.is_ok(), true);
assert_eq!(res.unwrap()["origin"], "47.13.94.134");
}

if let Ok(data) = res {
assert_eq!(data["origin"], "47.13.94.134");
}
#[test]
fn basic_post_request_body() {
let res = post_request_body(
"https://httpbin.org/post".to_string(),
None,
Some("filenames[]=img2.jpg".to_string()),
);
assert_eq!(res.is_ok(), true);
assert_eq!(res.unwrap()["data"], "filenames[]=img2.jpg");
}

#[test]
fn basic_post_request_multipart() {
let res = post_request_multipart(
"https://httpbin.org/post".to_string(),
None,
Some(vec!["./tests/fixtures/foo.html".to_string()]),
);
assert_eq!(res.is_ok(), true);
}
}
Loading

0 comments on commit 5e1cc23

Please sign in to comment.