Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add http client #1

Merged
merged 5 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rust-utils"
version = "0.1.0"
version = "0.2.0"
edition = "2021"

[lib]
Expand All @@ -13,19 +13,24 @@ lto = true
debug = true

[dependencies]
base64 = { version = "0.22.1", optional = true }
flume = { version = "0.11", optional = true }
once_cell = { version = "1.19", optional = true }
regex = { version = "1.10.4", optional = true }
regex-cache = { version = "0.2.1", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] }
serde_json = { version = "1.0", optional = true }
thiserror = "1.0.6"
translit = {version = "0.5.0", optional = true }
regex = {version = "1.10.4", optional = true }
regex-cache = {version = "0.2.1", optional = true }
base64 = {version = "0.22.1", optional = true}
translit = { version = "0.5.0", optional = true }
ureq = { version = "2.10.0", optional = true }

[features]
default = [
"transliteration",
"regexp",
"file"
]
default = ["transliteration", "regexp", "file", "http"]

transliteration = ["translit"]
regexp = ["regex", "regex-cache"]
file = ["base64"]
http = ["jobs", "once_cell", "serde", "serde_json", "ureq"]
regexp = ["regex", "regex-cache"]
transliteration = ["translit"]

# internal feature-like things
jobs = ["flume"]
13 changes: 13 additions & 0 deletions dmsrc/http.dm
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")()
8 changes: 7 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{io, result, str::Utf8Error};
use thiserror::Error;

pub type BResult<T> = result::Result<T, Error>;
pub type Result<T> = result::Result<T, Error>;

#[derive(Error, Debug)]
pub enum Error {
Expand All @@ -15,6 +15,12 @@ pub enum Error {
Io(#[from] io::Error),
#[error("Invalid algorithm specified.")]
InvalidAlgorithm,
#[cfg(feature = "http")]
#[error(transparent)]
JsonSerialization(#[from] serde_json::Error),
#[cfg(feature = "http")]
#[error(transparent)]
Request(#[from] Box<ureq::Error>),
}

impl From<Utf8Error> for Error {
Expand Down
4 changes: 2 additions & 2 deletions src/file.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::error::BResult;
use crate::error::Result;
use base64::Engine;
use std::{
fs::File,
Expand All @@ -12,7 +12,7 @@ byond_fn!(fn file_write(data, path, ...rest) {
write(data, path, should_decode_b64).err()
});

fn write(data: &str, path: &str, base64decode: bool) -> BResult<usize> {
fn write(data: &str, path: &str, base64decode: bool) -> Result<usize> {
let path: &std::path::Path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
Expand Down
182 changes: 182 additions & 0 deletions src/http.rs
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("")
}
);
64 changes: 64 additions & 0 deletions src/jobs.rs
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))
}
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ mod byond;
#[allow(dead_code)]
mod error;

#[cfg(feature = "jobs")]
mod jobs;

#[cfg(feature = "file")]
pub mod file;
#[cfg(feature = "http")]
pub mod http;
#[cfg(feature = "regexp")]
pub mod regexp;
#[cfg(feature = "transliteration")]
Expand Down
Loading