use std::marker::PhantomData;
use crate::error::Error;
use crate::source::{Probe, Source, SourceKind};
use crate::sources::util::{normalize, trim_trailing_slashes};
use crate::transport::HttpTransport;
pub trait CloudEndpoint: Send + Sync + 'static {
const DEBUG_NAME: &'static str;
const DEFAULT_BASE_URL: &'static str;
const PATH: &'static str;
const KIND: SourceKind;
fn headers() -> &'static [(&'static str, &'static str)];
}
pub struct CloudMetadata<E, T> {
transport: T,
base_url: String,
_endpoint: PhantomData<fn() -> E>,
}
impl<E: CloudEndpoint, T> CloudMetadata<E, T> {
pub fn new(transport: T) -> Self {
Self::with_base_url(transport, E::DEFAULT_BASE_URL)
}
pub fn with_base_url(transport: T, base_url: impl Into<String>) -> Self {
Self {
transport,
base_url: trim_trailing_slashes(base_url),
_endpoint: PhantomData,
}
}
}
impl<E: CloudEndpoint, T> std::fmt::Debug for CloudMetadata<E, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(E::DEBUG_NAME)
.field("base_url", &self.base_url)
.finish_non_exhaustive()
}
}
impl<E: CloudEndpoint, T: HttpTransport + 'static> Source for CloudMetadata<E, T> {
fn kind(&self) -> SourceKind {
E::KIND
}
fn probe(&self) -> Result<Option<Probe>, Error> {
let url = format!("{}{}", self.base_url, E::PATH);
let body = fetch_plaintext_id(&self.transport, &url, E::KIND, E::headers());
Ok(body
.as_deref()
.and_then(normalize)
.map(|v| Probe::new(E::KIND, v)))
}
}
fn fetch_plaintext_id<T: HttpTransport>(
transport: &T,
url: &str,
kind: SourceKind,
headers: &[(&str, &str)],
) -> Option<String> {
let mut builder = http::Request::builder().method(http::Method::GET).uri(url);
for (name, value) in headers {
builder = builder.header(*name, *value);
}
let request = builder.body(Vec::new()).ok()?;
let response = transport.send(request).ok()?;
if !response.status().is_success() {
log::debug!("{kind}: endpoint returned {}", response.status());
return None;
}
std::str::from_utf8(response.body()).ok().map(str::to_owned)
}
#[cfg(test)]
pub(crate) mod test_support {
use std::collections::VecDeque;
use std::convert::Infallible;
use std::sync::{Arc, Mutex};
use crate::transport::HttpTransport;
pub(crate) struct StubTransport {
inner: Mutex<Inner>,
}
struct Inner {
responses: VecDeque<http::Response<Vec<u8>>>,
requests: Vec<(http::Method, http::Uri, http::HeaderMap)>,
}
impl StubTransport {
pub(crate) fn new(responses: Vec<http::Response<Vec<u8>>>) -> Self {
Self {
inner: Mutex::new(Inner {
responses: responses.into(),
requests: Vec::new(),
}),
}
}
pub(crate) fn requests(&self) -> Vec<(http::Method, http::Uri, http::HeaderMap)> {
self.inner.lock().unwrap().requests.clone()
}
pub(crate) fn shared(
responses: Vec<http::Response<Vec<u8>>>,
) -> (Arc<Self>, impl HttpTransport + 'static) {
let stub = Arc::new(Self::new(responses));
let handle = Arc::clone(&stub);
let transport = move |req: http::Request<Vec<u8>>| handle.send(req);
(stub, transport)
}
}
impl HttpTransport for StubTransport {
type Error = Infallible;
fn send(
&self,
request: http::Request<Vec<u8>>,
) -> Result<http::Response<Vec<u8>>, Self::Error> {
let mut guard = self.inner.lock().unwrap();
let response = guard
.responses
.pop_front()
.expect("stub transport ran out of canned responses");
guard.requests.push((
request.method().clone(),
request.uri().clone(),
request.headers().clone(),
));
Ok(response)
}
}
pub(crate) fn ok(body: &str) -> http::Response<Vec<u8>> {
http::Response::builder()
.status(http::StatusCode::OK)
.body(body.as_bytes().to_vec())
.unwrap()
}
pub(crate) fn status(code: u16) -> http::Response<Vec<u8>> {
http::Response::builder()
.status(code)
.body(Vec::new())
.unwrap()
}
}