use crate::{error::BillectaErrorBody, Error, Uuid};
use std::{env, fmt::Write};
#[derive(Clone)]
pub struct Client {
c: reqwest::Client,
env: Env,
}
#[derive(Debug, Clone, Copy)]
enum Env {
Prod,
Test,
}
impl Env {
fn url(self) -> &'static str {
match self {
Self::Prod => "https://api.billecta.com",
Self::Test => "https://apitest.billecta.com",
}
}
}
impl Client {
pub fn from_env() -> Self {
let token = env::var("BILLECTA_TOKEN").expect("BILLECTA_TOKEN Not set");
let env = env::var("BILLECTA_ENV").unwrap_or_else(|_| String::from("prod"));
let env = match env.to_ascii_lowercase().as_str() {
"test" => Env::Test,
"prod" => Env::Prod,
invalid => panic!("invalid BILLECTA_ENV `{invalid}`"),
};
tracing::info!("Using Billecta {env:?} environment");
Self::new(env, token)
}
pub fn new_prod(token: impl AsRef<str>) -> Self {
Self::new(Env::Prod, token)
}
pub fn new_test(token: impl AsRef<str>) -> Self {
Self::new(Env::Test, token)
}
fn new(env: Env, token: impl AsRef<str>) -> Self {
static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
"Authorization",
format!("SecureToken {}", token.as_ref())
.parse()
.expect("Parsing token"),
);
headers.insert("Accept", http::HeaderValue::from_static("application/json"));
let c = reqwest::Client::builder()
.default_headers(headers)
.user_agent(USER_AGENT)
.build()
.expect("Building Client");
Self { c, env }
}
pub fn rate_limited(&self) -> crate::RateLimitedClient {
crate::RateLimitedClient::new(self.clone())
}
}
pub struct EmptyResponse;
pub struct Request<Reply> {
method: http::Method,
path: String,
body: Option<Vec<u8>>,
_marker: std::marker::PhantomData<Reply>,
}
async fn build_error(res: reqwest::Response) -> Error {
let status = res.status();
match status.as_u16() {
429 => Error::RateLimited,
404 => Error::NotFound,
_ => {
let text = match res.text().await {
Ok(text) => text,
Err(err) => {
return Error::Http(err);
}
};
match serde_json::from_str::<BillectaErrorBody>(&text) {
Ok(eb) => Error::Billecta {
status,
message: eb.message,
external_error_code: eb.external_error_code,
request_content: eb.request_content,
},
Err(_) => Error::Billecta {
status,
message: text,
external_error_code: None,
request_content: None,
},
}
}
}
}
impl Request<EmptyResponse> {
pub async fn send(self, client: &Client) -> Result<(), Error> {
let url = format!("{}/{}", client.env.url(), &self.path);
let mut builder = client.c.request(self.method, &url);
if let Some(body) = self.body {
let len = body.len();
builder = builder
.body(body)
.header("Content-Length", format!("{len}"))
.header("Content-Type", "application/json");
} else {
builder = builder.header("Content-Length", "0");
}
let res = builder.send().await?;
let status = res.status();
if !status.is_success() {
return Err(build_error(res).await);
}
Ok(())
}
}
impl<T> Request<T>
where
T: serde::de::DeserializeOwned,
{
pub async fn send(self, client: &Client) -> Result<T, Error> {
let url = format!("{}/{}", client.env.url(), &self.path);
let mut builder = client.c.request(self.method, &url);
if let Some(body) = self.body {
let len = body.len();
builder = builder
.body(body)
.header("Content-Length", format!("{len}"))
.header("Content-Type", "application/json");
} else {
builder = builder.header("Content-Length", "0");
}
let res = builder.send().await?;
let status = res.status();
if !status.is_success() {
return Err(build_error(res).await);
}
let text = res.text().await?;
let res = serde_json::from_str(&text).map_err(|err| Error::unexpected_json(err, &text))?;
Ok(res)
}
}
pub struct DataStream;
impl<DataStream> Request<DataStream> {
pub async fn send_for_data(self, client: &Client) -> Result<bytes::Bytes, Error> {
let url = format!("{}/{}", client.env.url(), &self.path);
let mut builder = client.c.request(self.method, &url);
if let Some(body) = self.body {
let len = body.len();
builder = builder
.body(body)
.header("Content-Length", format!("{len}"))
.header("Content-Type", "application/json");
} else {
builder = builder.header("Content-Length", "0");
}
let res = builder.send().await?;
let status = res.status();
if !status.is_success() {
return Err(build_error(res).await);
}
let data = res.bytes().await?;
Ok(data)
}
}
pub(crate) struct RequestBuilder {
method: http::Method,
path: String,
query: Option<String>,
body: Option<Vec<u8>>,
}
impl RequestBuilder {
pub(crate) fn new(method: http::Method, url: &str) -> Self {
Self {
method,
path: String::from(url),
query: None,
body: None,
}
}
pub(crate) fn path_param<P: AsPathParam>(mut self, param: P) -> Self {
self.path.push('/');
param.push_to(&mut self.path);
self
}
pub(crate) fn path_param_opt<P: AsPathParam>(self, param: Option<P>) -> Self {
if let Some(p) = param {
self.path_param(p)
} else {
self
}
}
pub(crate) fn query_param<P: AsPathParam>(mut self, name: &str, val: P) -> Self {
let buf = self.query.get_or_insert_with(String::new);
if buf.is_empty() {
buf.push('?');
} else {
buf.push('&');
}
buf.push_str(name);
buf.push('=');
val.push_to(buf);
self
}
pub(crate) fn query_param_opt<P: AsPathParam>(self, name: &str, val: Option<P>) -> Self {
if let Some(val) = val {
self.query_param(name, val)
} else {
self
}
}
pub fn body(mut self, body: &impl serde::Serialize) -> Self {
let bs = serde_json::to_vec(body).expect("Serializing body");
self.body = Some(bs);
self
}
pub(crate) fn build<T>(self) -> Request<T> {
let mut path = self.path;
if let Some(query) = self.query {
path.push_str(&query);
}
Request {
method: self.method,
path,
body: self.body,
_marker: std::marker::PhantomData,
}
}
}
pub trait AsPathParam {
fn push_to(&self, buf: &mut String);
}
impl AsPathParam for bool {
fn push_to(&self, buf: &mut String) {
write!(buf, "{self}").expect("Writing Param");
}
}
impl AsPathParam for i32 {
fn push_to(&self, buf: &mut String) {
write!(buf, "{self}").expect("Writing Param");
}
}
impl AsPathParam for i64 {
fn push_to(&self, buf: &mut String) {
write!(buf, "{self}").expect("Writing Param");
}
}
impl AsPathParam for f64 {
fn push_to(&self, buf: &mut String) {
write!(buf, "{self:.2}").expect("Writing Param");
}
}
impl AsPathParam for &str {
fn push_to(&self, buf: &mut String) {
write!(buf, "{self}").expect("Writing Param");
}
}
impl AsPathParam for Uuid {
fn push_to(&self, buf: &mut String) {
write!(buf, "{self}").expect("Writing Param");
}
}
impl AsPathParam for crate::Date {
fn push_to(&self, buf: &mut String) {
write!(
buf,
"{:04}-{:02}-{:02}",
self.year(),
self.month(),
self.day()
)
.expect("Writing Param");
}
}