use std::fmt;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use super::types::Subject;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BackendActionId(String);
impl BackendActionId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for BackendActionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, thiserror::Error)]
pub enum BackendError {
#[error("backend does not support this operation: {0}")]
Unsupported(&'static str),
#[error("network error: {0}")]
Network(String),
#[error("backend auth error: {0}")]
Auth(String),
#[error(
"backend rate limited: {message}{}",
retry_after_seconds.map(|s| format!(" (retry after {s}s)")).unwrap_or_default()
)]
RateLimited {
message: String,
retry_after_seconds: Option<u32>,
},
#[error("backend state conflict: {0}")]
Conflict(String),
#[error("backend remote error: code={code} message={message}")]
RemoteError {
code: String,
message: String,
},
#[error("backend validation error: {0}")]
Validation(String),
}
#[derive(Debug, Clone)]
pub struct ProbeReport {
pub backend_name: &'static str,
pub pds_url: String,
pub detected_version: Option<String>,
pub capabilities: Vec<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum BackendInitError {
#[error("failed to build HTTP client: {0}")]
HttpClient(String),
}
#[async_trait]
pub trait PdsAdminBackend: Send + Sync {
async fn takedown_account(
&self,
did: &str,
reason: &str,
notes: Option<&str>,
precipitating_action_id: i64,
) -> Result<BackendActionId, BackendError>;
async fn suspend_account(
&self,
did: &str,
reason: &str,
duration_days: Option<u32>,
notes: Option<&str>,
precipitating_action_id: i64,
) -> Result<BackendActionId, BackendError>;
async fn restore_account(
&self,
did: &str,
prior_action_id: &BackendActionId,
reason: &str,
) -> Result<(), BackendError>;
async fn apply_label(
&self,
subject: &Subject,
val: &str,
expires_days: Option<u32>,
) -> Result<(), BackendError>;
async fn negate_label(&self, subject: &Subject, val: &str) -> Result<(), BackendError>;
async fn probe(&self) -> Result<ProbeReport, BackendError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(dead_code)]
fn _assert_pds_admin_backend_send_sync() {
fn assert_send_sync<T: Send + Sync + ?Sized>() {}
assert_send_sync::<dyn PdsAdminBackend>();
}
#[allow(dead_code)]
fn _assert_backend_error_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<BackendError>();
}
#[allow(dead_code)]
fn _assert_backend_action_id_clone_eq_hash() {
fn assert_traits<T: Clone + Eq + std::hash::Hash>() {}
assert_traits::<BackendActionId>();
}
#[test]
fn backend_action_id_round_trip() {
let id = BackendActionId::new("backend-12345");
assert_eq!(id.as_str(), "backend-12345");
assert_eq!(format!("{id}"), "backend-12345");
}
#[test]
fn backend_action_id_eq_and_hash_consistent() {
let a = BackendActionId::new("xyz");
let b = BackendActionId::new(String::from("xyz"));
assert_eq!(a, b);
let mut map: std::collections::HashMap<BackendActionId, u32> =
std::collections::HashMap::new();
map.insert(a, 1);
assert_eq!(map.get(&b), Some(&1));
}
#[test]
fn backend_action_id_serde_roundtrip() {
let id = BackendActionId::new("backend-abc");
let json = serde_json::to_string(&id).unwrap();
let back: BackendActionId = serde_json::from_str(&json).unwrap();
assert_eq!(id, back);
}
#[test]
fn backend_error_unsupported_renders() {
let e = BackendError::Unsupported("OzoneBackend::apply_label");
assert_eq!(
format!("{e}"),
"backend does not support this operation: OzoneBackend::apply_label"
);
}
#[test]
fn backend_error_network_renders() {
let e = BackendError::Network("connection refused".into());
assert_eq!(format!("{e}"), "network error: connection refused");
}
#[test]
fn backend_error_auth_renders() {
let e = BackendError::Auth("HTTP 401".into());
assert_eq!(format!("{e}"), "backend auth error: HTTP 401");
}
#[test]
fn backend_error_rate_limited_with_retry_after_renders() {
let e = BackendError::RateLimited {
message: "too many requests".into(),
retry_after_seconds: Some(60),
};
assert_eq!(
format!("{e}"),
"backend rate limited: too many requests (retry after 60s)"
);
}
#[test]
fn backend_error_rate_limited_without_retry_after_renders() {
let e = BackendError::RateLimited {
message: "throttled".into(),
retry_after_seconds: None,
};
assert_eq!(format!("{e}"), "backend rate limited: throttled");
}
#[test]
fn backend_error_conflict_renders() {
let e = BackendError::Conflict("subject already taken down".into());
assert_eq!(
format!("{e}"),
"backend state conflict: subject already taken down"
);
}
#[test]
fn backend_error_remote_error_renders() {
let e = BackendError::RemoteError {
code: "InvalidRequest".into(),
message: "subject not a DID".into(),
};
assert_eq!(
format!("{e}"),
"backend remote error: code=InvalidRequest message=subject not a DID"
);
}
#[test]
fn backend_error_validation_renders() {
let e = BackendError::Validation("missing required field".into());
assert_eq!(
format!("{e}"),
"backend validation error: missing required field"
);
}
}