use serde_json::Value;
use crate::{AppDataDoc, AppDataError, DEFAULT_IPFS_READ_URI, IpfsConfig, app_data_hex_to_cid};
#[expect(
async_fn_in_trait,
reason = "the trait surface adopts native async fn in trait per ADR 0010 runtime-neutral posture; the resulting non-Send futures are covered by the workspace future_not_send allow so wasm callbacks can satisfy the same trait without an explicit Send bound"
)]
pub trait IpfsFetchTransport {
async fn get(&self, uri: &str) -> Result<String, AppDataError>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IpfsFetchPolicy {
read_base_uri: String,
}
impl Default for IpfsFetchPolicy {
fn default() -> Self {
Self {
read_base_uri: DEFAULT_IPFS_READ_URI.to_owned(),
}
}
}
impl IpfsFetchPolicy {
pub fn new(read_base_uri: impl Into<String>) -> Result<Self, AppDataError> {
let read_base_uri = read_base_uri.into();
Ok(Self {
read_base_uri: normalize_read_base_uri(&read_base_uri)?,
})
}
pub fn from_config(config: &IpfsConfig) -> Result<Self, AppDataError> {
let read_base_uri = config
.read_uri
.as_ref()
.map(|uri| uri.as_inner().as_str())
.or_else(|| config.uri.as_ref().map(|uri| uri.as_inner().as_str()))
.unwrap_or(DEFAULT_IPFS_READ_URI);
Self::new(read_base_uri)
}
#[must_use]
pub fn read_base_uri(&self) -> &str {
&self.read_base_uri
}
pub fn with_read_base_uri(
mut self,
read_base_uri: impl Into<String>,
) -> Result<Self, AppDataError> {
let read_base_uri = read_base_uri.into();
self.read_base_uri = normalize_read_base_uri(&read_base_uri)?;
Ok(self)
}
}
pub async fn fetch_doc_from_cid(
cid: &str,
transport: &impl IpfsFetchTransport,
ipfs_uri: Option<&str>,
) -> Result<AppDataDoc, AppDataError> {
fetch_doc_from_cid_with_policy(cid, transport, &policy_from_optional_uri(ipfs_uri)?).await
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
skip_all,
fields(
endpoint = "app_data.fetch_doc_from_cid",
cid = %cid,
),
),
)]
pub async fn fetch_doc_from_cid_with_policy(
cid: &str,
transport: &impl IpfsFetchTransport,
policy: &IpfsFetchPolicy,
) -> Result<AppDataDoc, AppDataError> {
let raw = transport
.get(&format!("{}/{}", policy.read_base_uri(), cid))
.await?;
serde_json::from_str::<Value>(&raw).map_err(AppDataError::from)
}
pub async fn fetch_doc_from_app_data_hex(
app_data_hex: &str,
transport: &impl IpfsFetchTransport,
ipfs_uri: Option<&str>,
) -> Result<AppDataDoc, AppDataError> {
fetch_doc_from_app_data_hex_with_policy(
app_data_hex,
transport,
&policy_from_optional_uri(ipfs_uri)?,
)
.await
}
pub async fn fetch_doc_from_app_data_hex_with_policy(
app_data_hex: &str,
transport: &impl IpfsFetchTransport,
policy: &IpfsFetchPolicy,
) -> Result<AppDataDoc, AppDataError> {
let cid = app_data_hex_to_cid(app_data_hex)?;
fetch_doc_from_cid_with_policy(&cid, transport, policy).await
}
fn policy_from_optional_uri(ipfs_uri: Option<&str>) -> Result<IpfsFetchPolicy, AppDataError> {
ipfs_uri.map_or_else(|| Ok(IpfsFetchPolicy::default()), IpfsFetchPolicy::new)
}
fn normalize_read_base_uri(read_base_uri: &str) -> Result<String, AppDataError> {
let normalized = read_base_uri.trim().trim_end_matches('/').to_owned();
if normalized.is_empty() {
return Err(AppDataError::Transport {
class: cow_sdk_core::TransportErrorClass::Builder,
detail: "ipfs read base uri must not be empty".to_owned().into(),
});
}
Ok(normalized)
}