-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add http client * format http.rs properly * update lib macro on byond side * fix tests * update byond macros and procs name
- Loading branch information
Showing
7 changed files
with
290 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// HTTP Operations // | ||
|
||
#define RUSTUTILS_HTTP_METHOD_GET "get" | ||
#define RUSTUTILS_HTTP_METHOD_PUT "put" | ||
#define RUSTUTILS_HTTP_METHOD_DELETE "delete" | ||
#define RUSTUTILS_HTTP_METHOD_PATCH "patch" | ||
#define RUSTUTILS_HTTP_METHOD_HEAD "head" | ||
#define RUSTUTILS_HTTP_METHOD_POST "post" | ||
#define rustutils_http_request_blocking(method, url, body, headers, options) CALL_LIB(RUST_UTILS, "http_request_blocking")(method, url, body, headers, options) | ||
#define rustutils_http_request_async(method, url, body, headers, options) CALL_LIB(RUST_UTILS, "http_request_async")(method, url, body, headers, options) | ||
#define rustutils_http_check_request(req_id) CALL_LIB(RUST_UTILS, "http_check_request")(req_id) | ||
/proc/rustutils_create_async_http_client() return CALL_LIB(RUST_UTILS, "start_http_client")() | ||
/proc/rustutils_close_async_http_client() return CALL_LIB(RUST_UTILS, "shutdown_http_client")() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
use crate::{error::Result, jobs}; | ||
use serde::{Deserialize, Serialize}; | ||
use std::cell::RefCell; | ||
use std::collections::{BTreeMap, HashMap}; | ||
use std::io::Write; | ||
|
||
// ---------------------------------------------------------------------------- | ||
// Interface | ||
|
||
#[derive(Deserialize)] | ||
struct RequestOptions { | ||
#[serde(default)] | ||
output_filename: Option<String>, | ||
#[serde(default)] | ||
body_filename: Option<String>, | ||
} | ||
|
||
#[derive(Serialize)] | ||
struct Response<'a> { | ||
status_code: u16, | ||
headers: HashMap<String, String>, | ||
body: Option<&'a str>, | ||
} | ||
|
||
// If the response can be deserialized -> success. | ||
// If the response can't be deserialized -> failure. | ||
byond_fn!(fn http_request_blocking(method, url, body, headers, options) { | ||
let req = match construct_request(method, url, body, headers, options) { | ||
Ok(r) => r, | ||
Err(e) => return Some(e.to_string()) | ||
}; | ||
|
||
match submit_request(req) { | ||
Ok(r) => Some(r), | ||
Err(e) => Some(e.to_string()) | ||
} | ||
}); | ||
|
||
// Returns new job-id. | ||
byond_fn!(fn http_request_async(method, url, body, headers, options) { | ||
let req = match construct_request(method, url, body, headers, options) { | ||
Ok(r) => r, | ||
Err(e) => return Some(e.to_string()) | ||
}; | ||
|
||
Some(jobs::start(move || { | ||
match submit_request(req) { | ||
Ok(r) => r, | ||
Err(e) => e.to_string() | ||
} | ||
})) | ||
}); | ||
|
||
// If the response can be deserialized -> success. | ||
// If the response can't be deserialized -> failure or WIP. | ||
byond_fn!(fn http_check_request(id) { | ||
Some(jobs::check(id)) | ||
}); | ||
|
||
// ---------------------------------------------------------------------------- | ||
// Shared HTTP client state | ||
|
||
const VERSION: &str = env!("CARGO_PKG_VERSION"); | ||
const PKG_NAME: &str = env!("CARGO_PKG_NAME"); | ||
|
||
thread_local! { | ||
pub static HTTP_CLIENT: RefCell<Option<ureq::Agent>> = RefCell::new(Some(ureq::agent())); | ||
} | ||
|
||
// ---------------------------------------------------------------------------- | ||
// Request construction and execution | ||
|
||
struct RequestPrep { | ||
req: ureq::Request, | ||
output_filename: Option<String>, | ||
body: Vec<u8>, | ||
} | ||
|
||
fn construct_request( | ||
method: &str, | ||
url: &str, | ||
body: &str, | ||
headers: &str, | ||
options: &str, | ||
) -> Result<RequestPrep> { | ||
HTTP_CLIENT.with(|cell| { | ||
let borrow = cell.borrow_mut(); | ||
match &*borrow { | ||
Some(client) => { | ||
let mut req = match method { | ||
"post" => client.post(url), | ||
"put" => client.put(url), | ||
"patch" => client.patch(url), | ||
"delete" => client.delete(url), | ||
"head" => client.head(url), | ||
_ => client.get(url), | ||
} | ||
.set("User-Agent", &format!("{PKG_NAME}/{VERSION}")); | ||
|
||
let mut final_body = body.as_bytes().to_vec(); | ||
|
||
if !headers.is_empty() { | ||
let headers: BTreeMap<&str, &str> = serde_json::from_str(headers)?; | ||
for (key, value) in headers { | ||
req = req.set(key, value); | ||
} | ||
} | ||
|
||
let mut output_filename = None; | ||
if !options.is_empty() { | ||
let options: RequestOptions = serde_json::from_str(options)?; | ||
output_filename = options.output_filename; | ||
if let Some(fname) = options.body_filename { | ||
final_body = std::fs::read(fname)?; | ||
} | ||
} | ||
|
||
Ok(RequestPrep { | ||
req, | ||
output_filename, | ||
body: final_body, | ||
}) | ||
} | ||
|
||
// If we got here we royally fucked up | ||
None => { | ||
let client = ureq::agent(); | ||
let req = client.get(""); | ||
let output_filename = None; | ||
Ok(RequestPrep { | ||
req, | ||
output_filename, | ||
body: Vec::new(), | ||
}) | ||
} | ||
} | ||
}) | ||
} | ||
|
||
fn submit_request(prep: RequestPrep) -> Result<String> { | ||
let response = prep.req.send_bytes(&prep.body).map_err(Box::new)?; | ||
|
||
let body; | ||
let mut resp = Response { | ||
status_code: response.status(), | ||
headers: HashMap::new(), | ||
body: None, | ||
}; | ||
|
||
for key in response.headers_names() { | ||
let Some(value) = response.header(&key) else { | ||
continue; | ||
}; | ||
|
||
resp.headers.insert(key, value.to_owned()); | ||
} | ||
|
||
if let Some(output_filename) = prep.output_filename { | ||
let mut writer = std::io::BufWriter::new(std::fs::File::create(output_filename)?); | ||
std::io::copy(&mut response.into_reader(), &mut writer)?; | ||
writer.flush()?; | ||
} else { | ||
body = response.into_string()?; | ||
resp.body = Some(&body); | ||
} | ||
|
||
Ok(serde_json::to_string(&resp)?) | ||
} | ||
|
||
byond_fn!( | ||
fn start_http_client() { | ||
HTTP_CLIENT.with(|cell| cell.replace(Some(ureq::agent()))); | ||
Some("") | ||
} | ||
); | ||
|
||
byond_fn!( | ||
fn shutdown_http_client() { | ||
HTTP_CLIENT.with(|cell| cell.replace(None)); | ||
Some("") | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
//! Job system | ||
use flume::Receiver; | ||
use std::{ | ||
cell::RefCell, | ||
collections::hash_map::{Entry, HashMap}, | ||
thread, | ||
}; | ||
|
||
struct Job { | ||
rx: Receiver<Output>, | ||
handle: thread::JoinHandle<()>, | ||
} | ||
|
||
type Output = String; | ||
type JobId = String; | ||
|
||
const NO_RESULTS_YET: &str = "NO RESULTS YET"; | ||
const NO_SUCH_JOB: &str = "NO SUCH JOB"; | ||
const JOB_PANICKED: &str = "JOB PANICKED"; | ||
|
||
#[derive(Default)] | ||
struct Jobs { | ||
map: HashMap<JobId, Job>, | ||
next_job: usize, | ||
} | ||
|
||
impl Jobs { | ||
fn start<F: FnOnce() -> Output + Send + 'static>(&mut self, f: F) -> JobId { | ||
let (tx, rx) = flume::unbounded(); | ||
let handle = thread::spawn(move || { | ||
let _ = tx.send(f()); | ||
}); | ||
let id = self.next_job.to_string(); | ||
self.next_job += 1; | ||
self.map.insert(id.clone(), Job { rx, handle }); | ||
id | ||
} | ||
|
||
fn check(&mut self, id: &str) -> Output { | ||
let entry = match self.map.entry(id.to_owned()) { | ||
Entry::Occupied(occupied) => occupied, | ||
Entry::Vacant(_) => return NO_SUCH_JOB.to_owned(), | ||
}; | ||
let result = match entry.get().rx.try_recv() { | ||
Ok(result) => result, | ||
Err(flume::TryRecvError::Disconnected) => JOB_PANICKED.to_owned(), | ||
Err(flume::TryRecvError::Empty) => return NO_RESULTS_YET.to_owned(), | ||
}; | ||
let _ = entry.remove().handle.join(); | ||
result | ||
} | ||
} | ||
|
||
thread_local! { | ||
static JOBS: RefCell<Jobs> = RefCell::default(); | ||
} | ||
|
||
pub fn start<F: FnOnce() -> Output + Send + 'static>(f: F) -> JobId { | ||
JOBS.with(|jobs| jobs.borrow_mut().start(f)) | ||
} | ||
|
||
pub fn check(id: &str) -> String { | ||
JOBS.with(|jobs| jobs.borrow_mut().check(id)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters