use super::ServiceEffectError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LysAnswer {
pub status: u16,
pub body: serde_json::Value,
}
impl LysAnswer {
#[must_use]
pub fn succeeded(&self) -> bool {
(200..300).contains(&self.status)
}
#[must_use]
pub fn refusal(&self) -> String {
self.body
.get("refusal")
.or_else(|| self.body.get("error"))
.and_then(serde_json::Value::as_str)
.map_or_else(|| format!("lys_status_{}", self.status), str::to_owned)
}
#[must_use]
pub fn refused(&self, route: &str) -> ServiceEffectError {
ServiceEffectError(format!("Lys refused {route}: {}", self.refusal()))
}
}
pub trait LysHttp: Send + Sync + std::fmt::Debug {
fn get(&self, bearer: &str, path: &str) -> Result<LysAnswer, ServiceEffectError>;
fn post(
&self,
bearer: &str,
path: &str,
body: &serde_json::Value,
) -> Result<LysAnswer, ServiceEffectError>;
}
fn could_not_be_asked(route: &str, error: reqwest::Error) -> ServiceEffectError {
ServiceEffectError(format!(
"lys_could_not_be_asked: {route}: {}",
error.without_url()
))
}
pub struct HttpLys {
http: reqwest::Client,
issuer: reqwest::Url,
handle: tokio::runtime::Handle,
}
impl std::fmt::Debug for HttpLys {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("HttpLys")
.field("issuer", &self.issuer.as_str())
.finish_non_exhaustive()
}
}
impl HttpLys {
pub fn new(issuer: &str, handle: tokio::runtime::Handle) -> Result<Self, ServiceEffectError> {
let mut issuer = reqwest::Url::parse(issuer)
.map_err(|error| ServiceEffectError(format!("Lys issuer is not a URL: {error}")))?;
if !matches!(issuer.scheme(), "http" | "https")
|| issuer.host_str().is_none()
|| !issuer.username().is_empty()
|| issuer.password().is_some()
|| issuer.query().is_some()
|| issuer.fragment().is_some()
{
return Err(ServiceEffectError(
"Lys issuer is not an HTTP origin".to_owned(),
));
}
if !issuer.path().ends_with('/') {
let path = format!("{}/", issuer.path());
issuer.set_path(&path);
}
Ok(Self {
http: reqwest::Client::new(),
issuer,
handle,
})
}
fn endpoint(&self, path: &str) -> Result<reqwest::Url, ServiceEffectError> {
self.issuer
.join(path.trim_start_matches('/'))
.map_err(|error| ServiceEffectError(format!("Lys route {path} refused: {error}")))
}
fn run(
&self,
route: &str,
request: reqwest::RequestBuilder,
) -> Result<LysAnswer, ServiceEffectError> {
let handle = &self.handle;
let future = async move {
let response = request
.send()
.await
.map_err(|error| could_not_be_asked(route, error))?;
let status = response.status().as_u16();
let bytes = response
.bytes()
.await
.map_err(|error| could_not_be_asked(route, error))?;
let body = if bytes.is_empty() {
serde_json::Value::Null
} else {
serde_json::from_slice(&bytes).map_err(|error| {
ServiceEffectError(format!(
"lys_could_not_be_asked: {route}: the answer is not JSON: {error}"
))
})?
};
Ok(LysAnswer { status, body })
};
std::thread::scope(|scope| scope.spawn(move || handle.block_on(future)).join())
.unwrap_or_else(|_| {
Err(ServiceEffectError(format!(
"lys_could_not_be_asked: {route}: the request did not complete"
)))
})
}
}
impl LysHttp for HttpLys {
fn get(&self, bearer: &str, path: &str) -> Result<LysAnswer, ServiceEffectError> {
let url = self.endpoint(path)?;
self.run(path, self.http.get(url).bearer_auth(bearer))
}
fn post(
&self,
bearer: &str,
path: &str,
body: &serde_json::Value,
) -> Result<LysAnswer, ServiceEffectError> {
let url = self.endpoint(path)?;
self.run(path, self.http.post(url).bearer_auth(bearer).json(body))
}
}
#[must_use]
pub fn segment(value: &str) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut encoded = String::with_capacity(value.len());
for byte in value.bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'~') {
encoded.push(char::from(byte));
} else {
encoded.push('%');
for nibble in [byte >> 4, byte & 0x0f] {
if let Some(digit) = HEX.get(usize::from(nibble)) {
encoded.push(char::from(*digit));
}
}
}
}
encoded
}