#![allow(
dead_code,
reason = "each platform's build uses its own part of this, and the tests all of it"
)]
use std::sync::Arc;
use std::time::Duration;
use leptoskit_query::BoxFuture;
use serde_json::Value;
use super::Api;
use super::ClientConfig;
use super::Key;
use crate::FnError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct HttpRequest {
pub url: String,
pub authorization: Option<String>,
pub idempotency_key: Option<String>,
pub body: String,
}
impl HttpRequest {
pub(crate) fn headers(&self) -> Vec<(&'static str, &str)> {
let mut headers = vec![
("accept", "application/json"),
("content-type", "application/json"),
];
if let Some(authorization) = &self.authorization {
headers.push(("authorization", authorization));
}
if let Some(key) = &self.idempotency_key {
headers.push(("idempotency-key", key));
}
headers
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct HttpResponse {
pub status: u16,
pub body: Vec<u8>,
}
pub(crate) trait Http: Send + Sync + 'static {
fn send(&self, request: HttpRequest) -> BoxFuture<'static, Result<HttpResponse, FnError>>;
}
pub trait Credentials: Send + Sync + 'static {
fn authorization(&self) -> BoxFuture<'_, Result<Option<String>, FnError>>;
fn refused(&self, sent: Option<String>) -> BoxFuture<'_, Result<bool, FnError>> {
let _ = sent;
Box::pin(std::future::ready(Ok(false)))
}
}
const KEYED_ATTEMPTS: u32 = 3;
pub(crate) struct Remote<H> {
inner: Arc<Inner<H>>,
}
struct Inner<H> {
origin: String,
http: H,
credentials: Option<Arc<dyn Credentials>>,
}
impl<H: Http> Remote<H> {
pub(crate) fn new(config: ClientConfig, http: H) -> Self {
Self {
inner: Arc::new(Inner {
origin: config
.origin
.unwrap_or_default()
.trim_end_matches('/')
.to_owned(),
http,
credentials: config.credentials,
}),
}
}
}
impl<H: Http> Inner<H> {
async fn authorization(&self) -> Result<Option<String>, FnError> {
match &self.credentials {
Some(credentials) => credentials.authorization().await,
None => Ok(None),
}
}
async fn send(
&self,
url: &str,
body: &str,
idempotency_key: Option<&String>,
) -> Result<HttpResponse, FnError> {
let request = |authorization| {
HttpRequest {
url: url.to_owned(),
authorization,
idempotency_key: idempotency_key.cloned(),
body: body.to_owned(),
}
};
let authorization = self.authorization().await?;
let response = self.http.send(request(authorization.clone())).await?;
if response.status != 401 {
return Ok(response);
}
let renewed = match &self.credentials {
Some(credentials) => credentials.refused(authorization).await?,
None => false,
};
if !renewed {
return Ok(response);
}
let authorization = self.authorization().await?;
self.http.send(request(authorization)).await
}
async fn call(
&self,
name: &'static str,
arguments: Value,
idempotency_key: Option<String>,
) -> Result<Value, FnError> {
let url = format!("{}/api/{name}", self.origin);
let body = arguments.to_string();
let attempts = if idempotency_key.is_some() {
KEYED_ATTEMPTS
} else {
1
};
let mut attempt = 0;
let response = loop {
attempt += 1;
match self.send(&url, &body, idempotency_key.as_ref()).await {
Err(FnError::Network(_)) if attempt < attempts => {
leptoskit_query::sleep(Duration::from_millis(250 * u64::from(attempt))).await;
}
outcome => break outcome?,
}
};
decode(&response)
}
}
impl<H: Http> Api for Remote<H> {
fn call(
&self,
name: &'static str,
arguments: Value,
key: Key,
) -> BoxFuture<'static, Result<Value, FnError>> {
let inner = Arc::clone(&self.inner);
Box::pin(async move { inner.call(name, arguments, key.resolve().await).await })
}
}
pub(crate) fn decode(response: &HttpResponse) -> Result<Value, FnError> {
let text = String::from_utf8_lossy(&response.body);
if !(200..300).contains(&response.status) {
if text.trim().is_empty() {
return Err(FnError::Other(format!(
"the server answered HTTP {}",
response.status
)));
}
return Err(FnError::from_wire(&text));
}
if text.trim().is_empty() {
return Ok(Value::Null);
}
serde_json::from_str(&text)
.map_err(|error| FnError::Decode(format!("the answer wasn't JSON: {error}")))
}
pub(crate) struct Unreachable;
impl Http for Unreachable {
fn send(&self, _request: HttpRequest) -> BoxFuture<'static, Result<HttpResponse, FnError>> {
Box::pin(std::future::ready(Err(FnError::Other(
"this build can't reach the server: turn on leptoskit_rpc's `web` feature for the \
browser, `guest` for a native app, or `server` with a database for server rendering"
.to_owned(),
))))
}
}