use std::time::Duration;
use reqwest::blocking::Client;
use reqwest::StatusCode;
use serde::Deserialize;
use url::Url;
use crate::diagnostics::{categories, Diagnostic, ValidationReport};
use crate::model::{RegisteredArtifact, Registry};
use crate::registry_net::types::PublishRequest;
use crate::registry_net::RegistryCache;
#[derive(Debug, thiserror::Error)]
pub enum RegistryClientError {
#[error("registry transport error: {0}")]
Transport(String),
#[error("registry HTTP {status}: {message}")]
Http {
status: u16,
message: String,
report: Option<ValidationReport>,
},
#[error("registry decode error: {0}")]
Decode(String),
}
impl RegistryClientError {
pub fn into_diagnostics(self) -> ValidationReport {
let mut report = ValidationReport::new();
match self {
Self::Transport(message) => report.push(
Diagnostic::error("DPCS-REGC-001", categories::REGISTRY_CLIENT, message)
.with_remediation("Check network connectivity and registry base URL"),
),
Self::Http {
status,
message,
report: server_report,
} => {
if let Some(server_report) = server_report {
report.extend(server_report);
}
report.push(
Diagnostic::error(
"DPCS-REGC-002",
categories::REGISTRY_CLIENT,
format!("HTTP {status}: {message}"),
)
.with_remediation("Inspect registry server logs and request payload"),
);
}
Self::Decode(message) => report.push(
Diagnostic::error("DPCS-REGC-003", categories::REGISTRY_CLIENT, message)
.with_remediation("Ensure the server speaks the DPCS reference registry API"),
),
}
report
}
pub fn is_transport(&self) -> bool {
matches!(self, Self::Transport(_))
}
pub fn is_decode(&self) -> bool {
matches!(self, Self::Decode(_))
}
pub fn is_server_error(&self) -> bool {
matches!(self, Self::Http { status, .. } if *status >= 500)
}
}
#[derive(Debug, Clone)]
pub struct RegistryClient {
base: Url,
http: Client,
token: Option<String>,
cache: RegistryCache,
}
impl RegistryClient {
pub fn new(base_url: impl AsRef<str>) -> Result<Self, RegistryClientError> {
let mut base = Url::parse(base_url.as_ref())
.map_err(|err| RegistryClientError::Transport(format!("invalid base URL: {err}")))?;
if !base.path().ends_with('/') {
let path = format!("{}/", base.path().trim_end_matches('/'));
base.set_path(&path);
}
let http = Client::builder()
.timeout(Duration::from_secs(30))
.build()
.map_err(|err| RegistryClientError::Transport(err.to_string()))?;
Ok(Self {
base,
http,
token: None,
cache: RegistryCache::memory(),
})
}
pub fn with_token(mut self, token: impl Into<String>) -> Self {
self.token = Some(token.into());
self
}
pub fn with_cache(mut self, cache: RegistryCache) -> Self {
self.cache = cache;
self
}
pub fn cache_mut(&mut self) -> &mut RegistryCache {
&mut self.cache
}
fn cache_namespace(&self) -> String {
let scheme = self.base.scheme();
let host = self.base.host_str().unwrap_or("localhost");
let port = self
.base
.port_or_known_default()
.map(|p| p.to_string())
.unwrap_or_default();
format!("{}://{}:{}{}", scheme, host, port, self.base.path())
}
fn absolute(
&self,
segments: &[&str],
query: &[(&str, &str)],
) -> Result<Url, RegistryClientError> {
let mut url = self.base.clone();
{
let mut path = url
.path_segments_mut()
.map_err(|_| RegistryClientError::Transport("cannot-be-a-base URL".into()))?;
path.pop_if_empty();
for segment in segments {
path.push(segment);
}
}
if !query.is_empty() {
let mut pairs = url.query_pairs_mut();
for (key, value) in query {
pairs.append_pair(key, value);
}
}
Ok(url)
}
fn artifact_url(
&self,
id: &str,
extra_segments: &[&str],
query: &[(&str, &str)],
) -> Result<Url, RegistryClientError> {
let mut segments = vec!["v1", "artifacts", id];
segments.extend_from_slice(extra_segments);
self.absolute(&segments, query)
}
fn request_url(&self, method: reqwest::Method, url: Url) -> reqwest::blocking::RequestBuilder {
let mut req = self.http.request(method, url);
if let Some(token) = &self.token {
req = req.bearer_auth(token);
}
req
}
pub fn get_registry(&self) -> Result<Registry, RegistryClientError> {
let url = self.absolute(&["v1", "registry"], &[])?;
let response = self
.request_url(reqwest::Method::GET, url)
.send()
.map_err(|err| RegistryClientError::Transport(err.to_string()))?;
Self::json(response)
}
pub fn list(
&self,
artifact_type: Option<&str>,
status: Option<&str>,
) -> Result<Vec<RegisteredArtifact>, RegistryClientError> {
let mut query = Vec::new();
if let Some(t) = artifact_type {
query.push(("type", t));
}
if let Some(s) = status {
query.push(("status", s));
}
let url = self.absolute(&["v1", "artifacts"], &query)?;
let response = self
.request_url(reqwest::Method::GET, url)
.send()
.map_err(|err| RegistryClientError::Transport(err.to_string()))?;
Self::json(response)
}
pub fn lookup(
&mut self,
id: &str,
version: Option<&str>,
) -> Result<RegisteredArtifact, RegistryClientError> {
let ns = self.cache_namespace();
if let Some(version) = version {
if let Some((artifact, _, _)) = self.cache.get(&ns, id, version) {
return Ok(artifact);
}
}
let mut query = Vec::new();
if let Some(v) = version {
query.push(("version", v));
}
let url = self.artifact_url(id, &[], &query)?;
let response = self
.request_url(reqwest::Method::GET, url)
.send()
.map_err(|err| RegistryClientError::Transport(err.to_string()))?;
let artifact: RegisteredArtifact = Self::json(response)?;
let _ = self.cache.put(&ns, artifact.clone(), None, None);
Ok(artifact)
}
pub fn fetch_content(
&mut self,
id: &str,
version: Option<&str>,
) -> Result<String, RegistryClientError> {
let ns = self.cache_namespace();
if let Some(version) = version {
if let Some((_, Some(content), _)) = self.cache.get(&ns, id, version) {
return Ok(content);
}
}
let mut query = Vec::new();
if let Some(v) = version {
query.push(("version", v));
}
let url = self.artifact_url(id, &["content"], &query)?;
let response = self
.request_url(reqwest::Method::GET, url)
.send()
.map_err(|err| RegistryClientError::Transport(err.to_string()))?;
let status = response.status();
let body = response
.text()
.map_err(|err| RegistryClientError::Transport(err.to_string()))?;
if !status.is_success() {
return Err(RegistryClientError::Http {
status: status.as_u16(),
message: body,
report: None,
});
}
if let Some(version) = version {
if let Some((artifact, _, etag)) = self.cache.get(&ns, id, version) {
let _ = self.cache.put(&ns, artifact, Some(body.clone()), etag);
}
}
Ok(body)
}
pub fn publish(
&mut self,
id: &str,
request: &PublishRequest,
) -> Result<RegisteredArtifact, RegistryClientError> {
let url = self.artifact_url(id, &[], &[])?;
let response = self
.request_url(reqwest::Method::PUT, url)
.json(request)
.send()
.map_err(|err| RegistryClientError::Transport(err.to_string()))?;
let artifact = Self::json(response)?;
let _ = self.cache.clear();
Ok(artifact)
}
pub fn deprecate(
&mut self,
id: &str,
version: Option<&str>,
) -> Result<RegisteredArtifact, RegistryClientError> {
let mut query = Vec::new();
if let Some(v) = version {
query.push(("version", v));
}
let url = self.artifact_url(id, &["deprecate"], &query)?;
let response = self
.request_url(reqwest::Method::POST, url)
.send()
.map_err(|err| RegistryClientError::Transport(err.to_string()))?;
let artifact = Self::json(response)?;
let _ = self.cache.clear();
Ok(artifact)
}
pub fn retire(
&mut self,
id: &str,
version: Option<&str>,
) -> Result<RegisteredArtifact, RegistryClientError> {
let mut query = Vec::new();
if let Some(v) = version {
query.push(("version", v));
}
let url = self.artifact_url(id, &["retire"], &query)?;
let response = self
.request_url(reqwest::Method::POST, url)
.send()
.map_err(|err| RegistryClientError::Transport(err.to_string()))?;
let artifact = Self::json(response)?;
let _ = self.cache.clear();
Ok(artifact)
}
fn json<T: for<'de> Deserialize<'de>>(
response: reqwest::blocking::Response,
) -> Result<T, RegistryClientError> {
let status = response.status();
let body = response
.text()
.map_err(|err| RegistryClientError::Transport(err.to_string()))?;
if status == StatusCode::BAD_REQUEST {
let report = serde_json::from_str::<ValidationReport>(&body).ok();
return Err(RegistryClientError::Http {
status: status.as_u16(),
message: body,
report,
});
}
if !status.is_success() {
return Err(RegistryClientError::Http {
status: status.as_u16(),
message: body,
report: None,
});
}
serde_json::from_str(&body).map_err(|err| RegistryClientError::Decode(err.to_string()))
}
}