use std::sync::Arc;
use crate::application::{
DeleteFileUseCase, ImageParams, PublicUrlBuilder, PublicUrlOptions, ReadFileUseCase,
ReadOptions, WriteFileUseCase, WriteOptions, WriteResult,
};
use crate::domain::{DomainError, DomainResult, FileId, LookupResult};
use crate::infrastructure::{
create_http_client, HaMasterClient, HaMasterClientBuilder, HttpClientConfig, HttpVolumeClient,
MasterSelectionStrategy,
};
#[derive(Debug, Default)]
pub struct WeedClientBuilder {
master_urls: Vec<String>,
strategy: MasterSelectionStrategy,
max_retries: usize,
http_config: HttpClientConfig,
}
impl WeedClientBuilder {
#[must_use]
pub fn new() -> Self {
Self {
master_urls: Vec::new(),
strategy: MasterSelectionStrategy::RoundRobin,
max_retries: 3,
http_config: HttpClientConfig::default(),
}
}
#[must_use]
pub fn master_url(mut self, url: impl Into<String>) -> Self {
self.master_urls.push(url.into());
self
}
#[must_use]
pub fn master_urls<I, S>(mut self, urls: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.master_urls.extend(urls.into_iter().map(Into::into));
self
}
#[must_use]
pub const fn strategy(mut self, strategy: MasterSelectionStrategy) -> Self {
self.strategy = strategy;
self
}
#[must_use]
pub const fn max_retries(mut self, max_retries: usize) -> Self {
self.max_retries = max_retries;
self
}
#[must_use]
pub fn http_config(mut self, config: HttpClientConfig) -> Self {
self.http_config = config;
self
}
pub fn build(self) -> DomainResult<WeedClient> {
if self.master_urls.is_empty() {
return Err(DomainError::ConfigurationError {
reason: "At least one master URL is required".to_string(),
});
}
let http_client =
create_http_client(&self.http_config).map_err(|e| DomainError::ConfigurationError {
reason: format!("Failed to create HTTP client: {e}"),
})?;
let master = HaMasterClientBuilder::new()
.master_urls(self.master_urls)
.strategy(self.strategy)
.max_retries(self.max_retries)
.build(http_client.clone())?;
let volume = HttpVolumeClient::new(http_client);
Ok(WeedClient {
master: Arc::new(master),
volume: Arc::new(volume),
})
}
pub fn build_blocking(self) -> DomainResult<BlockingWeedClient> {
let client = self.build()?;
let runtime =
tokio::runtime::Runtime::new().map_err(|e| DomainError::ConfigurationError {
reason: format!("Failed to create runtime: {e}"),
})?;
Ok(BlockingWeedClient { client, runtime })
}
}
#[derive(Clone)]
pub struct WeedClient {
master: Arc<HaMasterClient>,
volume: Arc<HttpVolumeClient>,
}
impl WeedClient {
#[must_use]
pub fn builder() -> WeedClientBuilder {
WeedClientBuilder::new()
}
pub async fn write(&self, data: Vec<u8>, filename: Option<&str>) -> DomainResult<FileId> {
let options = filename.map(WriteOptions::with_filename);
let use_case = WriteFileUseCase::new(self.master.as_ref(), self.volume.as_ref());
let result = use_case.execute(data, options).await?;
Ok(result.file_id)
}
pub async fn write_with_options(
&self,
data: Vec<u8>,
options: WriteOptions,
) -> DomainResult<WriteResult> {
let use_case = WriteFileUseCase::new(self.master.as_ref(), self.volume.as_ref());
use_case.execute(data, Some(options)).await
}
pub async fn read(&self, file_id: &FileId) -> DomainResult<Vec<u8>> {
let use_case = ReadFileUseCase::new(self.master.as_ref(), self.volume.as_ref());
let result = use_case.execute(file_id, None).await?;
Ok(result.data)
}
pub async fn read_with_options(
&self,
file_id: &FileId,
options: ReadOptions,
) -> DomainResult<Vec<u8>> {
let use_case = ReadFileUseCase::new(self.master.as_ref(), self.volume.as_ref());
let result = use_case.execute(file_id, Some(options)).await?;
Ok(result.data)
}
pub async fn delete(&self, file_id: &FileId) -> DomainResult<()> {
let use_case = DeleteFileUseCase::new(self.master.as_ref(), self.volume.as_ref());
use_case.execute(file_id).await
}
pub async fn public_url(&self, file_id: &FileId) -> DomainResult<String> {
let builder = PublicUrlBuilder::new(self.master.as_ref());
builder.build(file_id, None).await
}
pub async fn public_url_resized(
&self,
file_id: &FileId,
width: u32,
height: u32,
) -> DomainResult<String> {
let builder = PublicUrlBuilder::new(self.master.as_ref());
let options = PublicUrlOptions {
image_params: Some(ImageParams::dimensions(width, height)),
prefer_public: true,
};
builder.build(file_id, Some(options)).await
}
pub async fn lookup(&self, file_id: &FileId) -> DomainResult<LookupResult> {
let use_case = ReadFileUseCase::new(self.master.as_ref(), self.volume.as_ref());
use_case.lookup(file_id).await
}
pub fn parse_file_id(fid: &str) -> DomainResult<FileId> {
FileId::parse(fid)
}
}
pub struct BlockingWeedClient {
client: WeedClient,
runtime: tokio::runtime::Runtime,
}
impl BlockingWeedClient {
#[must_use]
pub fn builder() -> WeedClientBuilder {
WeedClientBuilder::new()
}
pub fn write(&self, data: Vec<u8>, filename: Option<&str>) -> DomainResult<FileId> {
self.runtime.block_on(self.client.write(data, filename))
}
pub fn write_with_options(
&self,
data: Vec<u8>,
options: WriteOptions,
) -> DomainResult<WriteResult> {
self.runtime
.block_on(self.client.write_with_options(data, options))
}
pub fn read(&self, file_id: &FileId) -> DomainResult<Vec<u8>> {
self.runtime.block_on(self.client.read(file_id))
}
pub fn read_with_options(
&self,
file_id: &FileId,
options: ReadOptions,
) -> DomainResult<Vec<u8>> {
self.runtime
.block_on(self.client.read_with_options(file_id, options))
}
pub fn delete(&self, file_id: &FileId) -> DomainResult<()> {
self.runtime.block_on(self.client.delete(file_id))
}
pub fn public_url(&self, file_id: &FileId) -> DomainResult<String> {
self.runtime.block_on(self.client.public_url(file_id))
}
pub fn public_url_resized(
&self,
file_id: &FileId,
width: u32,
height: u32,
) -> DomainResult<String> {
self.runtime
.block_on(self.client.public_url_resized(file_id, width, height))
}
pub fn lookup(&self, file_id: &FileId) -> DomainResult<LookupResult> {
self.runtime.block_on(self.client.lookup(file_id))
}
pub fn parse_file_id(fid: &str) -> DomainResult<FileId> {
FileId::parse(fid)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_no_urls() {
let result = WeedClientBuilder::new().build();
assert!(result.is_err());
}
#[test]
fn test_builder_with_url() {
let result = WeedClientBuilder::new()
.master_url("http://localhost:9333")
.build();
assert!(result.is_ok());
}
#[test]
fn test_builder_multiple_urls() {
let result = WeedClientBuilder::new()
.master_urls(["http://master1:9333", "http://master2:9333"])
.strategy(MasterSelectionStrategy::Failover)
.max_retries(5)
.build();
assert!(result.is_ok());
}
#[test]
fn test_parse_file_id() {
let result = WeedClient::parse_file_id("3,0000016300007037");
assert!(result.is_ok());
}
#[test]
fn test_parse_file_id_invalid() {
let result = WeedClient::parse_file_id("invalid");
assert!(result.is_err());
}
}