pub mod sse;
pub mod utils;
pub use chrono::{self};
pub use reqwest::{self, StatusCode};
pub use serde_json::{self};
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
#[derive(Clone)]
pub struct ArriClientConfig {
pub http_client: reqwest::Client,
pub base_url: String,
pub headers: HashMap<&'static str, String>,
}
#[derive(Clone)]
pub struct InternalArriClientConfig {
pub http_client: reqwest::Client,
pub base_url: String,
pub headers: Arc<RwLock<HashMap<&'static str, String>>>,
}
pub trait ArriClientService {
fn create(config: ArriClientConfig) -> Self;
fn update_headers(&self, headers: HashMap<&'static str, String>);
}
impl InternalArriClientConfig {
pub fn from(config: ArriClientConfig) -> Self {
Self {
http_client: config.http_client,
base_url: config.base_url,
headers: Arc::new(RwLock::new(config.headers)),
}
}
}
pub struct ArriRequestOptions<'a> {
pub http_client: &'a reqwest::Client,
pub url: String,
pub method: reqwest::Method,
pub headers: Arc<RwLock<HashMap<&'static str, String>>>,
pub client_version: String,
}
pub struct ArriParsedRequestOptions<'a> {
pub http_client: &'a reqwest::Client,
pub url: String,
pub method: reqwest::Method,
pub headers: Arc<RwLock<HashMap<&'static str, String>>>,
pub client_version: String,
}
#[derive(Debug)]
pub struct ArriServerError {
pub code: u16,
pub message: String,
pub stack: Option<String>,
pub data: Option<serde_json::Value>,
}
trait ArriRequestErrorMethods {
fn from_response_data(status: u16, body: String) -> Self;
}
impl ArriRequestErrorMethods for ArriServerError {
fn from_response_data(status: u16, body: String) -> Self {
let mut err = Self::from_json_string(body.to_owned());
if err.code == 0 {
err.code = status.to_owned();
}
if err.message.is_empty() {
err.message = status_message_from_status_code(err.code);
}
err
}
}
impl ArriModel for ArriServerError {
fn new() -> Self {
Self {
code: 0,
message: "".to_string(),
stack: None,
data: None,
}
}
fn from_json(input: serde_json::Value) -> Self {
match input {
serde_json::Value::Object(val) => {
let code = match val.get("code") {
Some(serde_json::Value::Number(status_code_val)) => {
u16::try_from(status_code_val.to_owned().as_u64().unwrap_or_default())
.unwrap_or(0)
}
_ => 0,
};
let message = match val.get("message") {
Some(serde_json::Value::String(status_message_val)) => {
status_message_val.to_owned()
}
_ => "Unknown error".to_string(),
};
let stack = match val.get("stack") {
Some(serde_json::Value::String(stack_val)) => Some(stack_val.to_owned()),
_ => None,
};
let data = match val.get("data") {
Some(data_val) => Some(data_val.to_owned()),
_ => None,
};
Self {
code,
message,
stack,
data,
}
}
_ => Self::new(),
}
}
fn from_json_string(input: String) -> Self {
match serde_json::from_str(input.as_str()) {
Ok(val) => Self::from_json(val),
_ => Self::new(),
}
}
fn to_json_string(&self) -> String {
let mut result = "{".to_string();
result.push_str("\"code\":");
result.push_str(format!("{}", &self.code).as_str());
result.push_str(",\"message\":");
result.push_str(
format!(
"\"{}\"",
&self.message.replace("\n", "\\n").replace("\"", "\\\"")
)
.as_str(),
);
match &self.stack {
Some(val) => {
result.push_str(",\"stack\":");
result.push_str(
format!("\"{}\"", val.replace("\n", "\\n").replace("\"", "\\\"")).as_str(),
);
}
_ => {}
}
match &self.data {
Some(val) => {
result.push_str(",\"data\":");
result.push_str(
serde_json::to_string(val)
.unwrap_or("null".to_string())
.as_str(),
)
}
_ => {}
}
result
}
fn to_query_params_string(&self) -> String {
let mut query_parts: Vec<String> = Vec::new();
query_parts.push(format!("code={}", &self.code));
query_parts.push(format!("message={}", &self.message));
match &self.stack {
Some(stack) => {
query_parts.push(format!("stack={}", stack));
}
_ => {}
}
match &self.data {
Some(data) => {
query_parts.push(format!(
"data={}",
serde_json::to_string(data).unwrap_or("null".to_string())
));
}
_ => {}
}
query_parts.join("&")
}
}
pub async fn arri_request<'a>(
opts: ArriRequestOptions<'a>,
params: Option<impl ArriModel>,
) -> Result<reqwest::Response, ArriServerError> {
let response: Result<reqwest::Response, reqwest::Error>;
let mut headers: HashMap<&str, String> = HashMap::new();
{
let unlocked = opts.headers.read().unwrap();
for (key, val) in unlocked.iter() {
headers.insert(*key, val.clone().to_owned());
}
}
match headers.get("Accept") {
Some(_) => {}
None => {
headers.insert("Accept", "application/json".to_string());
}
}
if !opts.client_version.is_empty() {
headers.insert("client-version", opts.client_version);
}
if opts.method != reqwest::Method::GET && opts.method != reqwest::Method::HEAD {
headers.insert("Content-Type", "application/json".to_string());
}
let mut final_headers = reqwest::header::HeaderMap::new();
for (key, value) in headers {
match reqwest::header::HeaderValue::from_str(value.as_str()) {
Ok(header_val) => {
final_headers.insert(key, header_val);
}
Err(_) => {
println!(
"WARNING: Received invalid header value. key: \"{}\", value: \"{}\"",
key, value,
);
}
}
}
match opts.method {
reqwest::Method::GET => {
let mut final_url = opts.url.clone();
match params {
Some(val) => final_url = format!("{final_url}?{}", val.to_query_params_string()),
None => {}
}
response = opts
.http_client
.get(final_url)
.headers(final_headers)
.send()
.await;
}
reqwest::Method::POST => {
let builder = opts
.http_client
.post(opts.url.clone())
.headers(final_headers);
match params {
Some(val) => {
response = builder.body(val.to_json_string()).send().await;
}
None => {
response = builder.send().await;
}
}
}
reqwest::Method::PUT => {
let builder = opts
.http_client
.put(opts.url.clone())
.headers(final_headers);
match params {
Some(val) => {
response = builder.body(val.to_json_string()).send().await;
}
None => {
response = builder.send().await;
}
}
}
reqwest::Method::PATCH => {
let builder = opts
.http_client
.patch(opts.url.clone())
.headers(final_headers);
match params {
Some(val) => {
response = builder.body(val.to_json_string()).send().await;
}
None => {
response = builder.send().await;
}
}
}
reqwest::Method::DELETE => {
let builder = opts
.http_client
.delete(opts.url.clone())
.headers(final_headers);
match params {
Some(val) => {
response = builder.body(val.to_json_string()).send().await;
}
None => {
response = builder.send().await;
}
}
}
_ => panic!("Unsupported method"),
};
match response {
Ok(res) => return Ok(res),
Err(err) => {
return Err(ArriServerError {
code: err.status().unwrap_or(StatusCode::default()).as_u16(),
message: format!("Error requesting \"{}\"", opts.url),
stack: None,
data: None,
})
}
}
}
pub trait ArriModel {
fn new() -> Self;
fn from_json(input: serde_json::Value) -> Self;
fn from_json_string(input: String) -> Self;
fn to_json_string(&self) -> String;
fn to_query_params_string(&self) -> String;
}
pub trait ArriEnum {
fn default() -> Self;
fn from_string(input: String) -> Self;
fn serial_value(&self) -> String;
}
#[derive(Debug, Clone)]
pub struct EmptyArriModel {}
impl ArriModel for EmptyArriModel {
fn new() -> Self {
Self {}
}
fn from_json(_: serde_json::Value) -> Self {
Self {}
}
fn from_json_string(_: String) -> Self {
Self {}
}
fn to_json_string(&self) -> String {
"{}".to_string()
}
fn to_query_params_string(&self) -> String {
"".to_string()
}
}
pub trait ArriService {
fn new() -> Self;
}
pub async fn parsed_arri_request<'a, TResponse>(
opts: ArriParsedRequestOptions<'a>,
params: Option<impl ArriModel>,
parser: fn(body: String) -> TResponse,
) -> Result<TResponse, ArriServerError> {
let result = arri_request(
ArriRequestOptions {
method: opts.method,
url: opts.url,
http_client: opts.http_client,
headers: opts.headers,
client_version: opts.client_version,
},
params,
)
.await;
if result.is_err() {
return Err(result.unwrap_err());
}
let response = result.unwrap();
let status = response.status().as_u16();
let body: Result<String, reqwest::Error> = response.text().await;
if status >= 300 || status < 200 {
return Err(ArriServerError::from_response_data(
status,
body.unwrap_or_default(),
));
}
match body {
Ok(text) => return Ok(parser(text)),
Err(err) => {
return Err(ArriServerError {
code: status,
message: "Expected server to return plaintext".to_string(),
stack: None,
data: Some(serde_json::Value::String(err.to_string())),
})
}
}
}
fn status_message_from_status_code(status_code: u16) -> String {
match status_code {
100 => String::from("Continue"),
101 => String::from("Switching Protocols"),
102 => String::from("Processing"),
103 => String::from("Early Hints"),
200 => String::from("OK"),
201 => String::from("Created"),
202 => String::from("Accepted"),
203 => String::from("Non-Authoritative Information"),
204 => String::from("No Content"),
205 => String::from("Reset Content"),
206 => String::from("Partial Content"),
207 => String::from("Multi-Status"),
208 => String::from("Already Reported"),
226 => String::from("IM Used"),
300 => String::from("Multiple Choices"),
301 => String::from("Moved Permanently"),
302 => String::from("Found"),
303 => String::from("See Other"),
304 => String::from("Not Modified"),
305 => String::from("Use Proxy"),
306 => String::from("unused"),
307 => String::from("Temporary Redirect"),
308 => String::from("Permanent Redirect"),
400 => String::from("Bad Request"),
401 => String::from("Unauthorized"),
402 => String::from("Payment Required"),
403 => String::from("Forbidden"),
404 => String::from("Not Found"),
405 => String::from("Method Not Allowed"),
406 => String::from("Not Acceptable"),
407 => String::from("Proxy Authentication Required"),
408 => String::from("Request Timeout"),
409 => String::from("Conflict"),
410 => String::from("Gone"),
411 => String::from("Length Required"),
412 => String::from("Precondition Failed"),
413 => String::from("Payload Too Large"),
414 => String::from("URI Too Long"),
415 => String::from("Unsupported Media Type"),
416 => String::from("Range Not Satisfiable"),
417 => String::from("Expectation Failed"),
418 => String::from("I'm a teapot"),
421 => String::from("Misdirected Request"),
422 => String::from("Unprocessable Content"),
423 => String::from("Locked"),
424 => String::from("Failed Dependency"),
425 => String::from("Too Early"),
426 => String::from("Upgrade Required"),
428 => String::from("Too Many Requests"),
431 => String::from("Request Header Fields Too Large"),
451 => String::from("Unavailable For Legal Reasons"),
500 => String::from("Internal Server Error"),
501 => String::from("Not Implemented"),
502 => String::from("Bad Gateway"),
503 => String::from("Service Unavailable"),
504 => String::from("Gateway Timeout"),
505 => String::from("HTTP Version Not Supported"),
506 => String::from("Variant Also Negotiates"),
507 => String::from("Insufficient Storage"),
508 => String::from("Loop Detected"),
510 => String::from("Not Extended"),
511 => String::from("Network Authentication Required"),
_ => String::from("Unknown Error"),
}
}