spiral-sdk 0.1.7

Rust SDK for Spiral resource and table APIs
Documentation
// Generated by tools/service-contracts/rust.py. Do not edit.
#![doc = "Generated reqwest client with typed errors and streaming bodies."]

use async_trait::async_trait;
use reqwest::{Client, Url};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use spiral_contract::auth::AuthHeader;
use spiral_contract::call::{CallError, ServiceFailure};
use spiral_contract::error::ErrorEnvelope;
use uuid::Uuid;

use crate::api::enrichments::EnrichmentsApi;
use crate::api::enrichments::objects::*;
/// HTTP client for the enrichments service.
#[derive(Clone)]
pub struct EnrichmentsClient {
    http: ClientWithMiddleware,
    base: Url,
}

impl std::fmt::Debug for EnrichmentsClient {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("EnrichmentsClient")
            .field("base", &self.base)
            .finish_non_exhaustive()
    }
}

impl EnrichmentsClient {
    /// Create a redirect-disabled client; base may contain a deployment prefix.
    #[expect(
        clippy::disallowed_methods,
        reason = "Generated clients keep their transport independent of the Spiral application runtime."
    )]
    pub fn new(base: &str) -> Result<Self, CallError<EnrichmentsError>> {
        let http = Client::builder()
            .redirect(reqwest::redirect::Policy::none())
            .connect_timeout(std::time::Duration::from_secs(10))
            .build()
            .map_err(CallError::transport)?;
        Self::from_http(base, http)
    }
    /// Use an existing connection pool with redirects disabled.
    pub fn from_http(base: &str, http: Client) -> Result<Self, CallError<EnrichmentsError>> {
        Self::from_transport(base, ClientBuilder::new(http).build())
    }
    /// Use a host-owned connection pool and instrumentation. Redirects must be disabled.
    pub fn from_transport(
        base: &str,
        http: ClientWithMiddleware,
    ) -> Result<Self, CallError<EnrichmentsError>> {
        let base = Url::parse(base)
            .map_err(|_| CallError::<EnrichmentsError>::protocol("invalid base URL"))?;
        if !matches!(base.scheme(), "http" | "https")
            || base.host_str().is_none()
            || !base.username().is_empty()
            || base.password().is_some()
            || base.query().is_some()
            || base.fragment().is_some()
        {
            return Err(CallError::<EnrichmentsError>::protocol("invalid base URL"));
        }
        Ok(Self { http, base })
    }
    /// Replace the host-owned connection pool and instrumentation. Redirects must be disabled.
    pub fn with_transport(mut self, http: ClientWithMiddleware) -> Self {
        self.http = http;
        self
    }
    /// Supply transport instrumentation or custom connection policy.
    ///
    /// The caller must disable redirects to protect bearer credentials.
    pub fn with_http(mut self, http: Client) -> Self {
        self.http = ClientBuilder::new(http).build();
        self
    }
    /// Allocate an Enrichment root and private backing storage for its source Table.
    pub async fn create_enrichment_http(
        &self,
        auth_header: AuthHeader,
        request: CreateEnrichmentRequest,
    ) -> Result<CreateEnrichmentResponse, CallError<EnrichmentsError>> {
        let mut url = self.base.clone();
        {
            let mut path = url.path_segments_mut().map_err(|_| {
                CallError::<EnrichmentsError>::protocol("base URL cannot contain path segments")
            })?;
            path.pop_if_empty();
            path.push("api");
            path.push("enrichments");
        }
        let builder = self.http.post(url).header("accept", "application/json");
        let builder = builder.header("authorization", auth_header.as_str());
        let builder = builder
            .header("content-type", "application/json")
            .json(&request);
        let response = self.send(builder, "application/json").await?;
        let status = response.status().as_u16();
        let bytes = bounded_body(response, 8 * 1024 * 1024).await?;
        serde_json::from_slice(&bytes)
            .map_err(|_| CallError::protocol_response(status, "invalid JSON response"))
    }
    /// Record an immutable successor of an explicit base. The service structurally validates codec
    /// payloads without initializing an execution engine.
    pub async fn create_version_http(
        &self,
        auth_header: AuthHeader,
        enrichment_id: Uuid,
        request: CreateVersionRequest,
    ) -> Result<CreateVersionResponse, CallError<EnrichmentsError>> {
        let mut url = self.base.clone();
        {
            let mut path = url.path_segments_mut().map_err(|_| {
                CallError::<EnrichmentsError>::protocol("base URL cannot contain path segments")
            })?;
            path.pop_if_empty();
            path.push("api");
            path.push("enrichments");
            let segment = enrichment_id.to_string();
            if matches!(segment.as_str(), "." | "..") {
                return Err(CallError::<EnrichmentsError>::protocol("dot path segment"));
            }
            path.push(&segment);
            path.push("versions");
        }
        let builder = self.http.post(url).header("accept", "application/json");
        let builder = builder.header("authorization", auth_header.as_str());
        let builder = builder
            .header("content-type", "application/json")
            .json(&request);
        let response = self.send(builder, "application/json").await?;
        let status = response.status().as_u16();
        let bytes = bounded_body(response, 8 * 1024 * 1024).await?;
        serde_json::from_slice(&bytes)
            .map_err(|_| CallError::protocol_response(status, "invalid JSON response"))
    }
    /// Read the Enrichment root and one exact immutable version.
    pub async fn read_version_http(
        &self,
        auth_header: AuthHeader,
        enrichment_id: Uuid,
        version_id: Uuid,
    ) -> Result<ReadVersionResponse, CallError<EnrichmentsError>> {
        let mut url = self.base.clone();
        {
            let mut path = url.path_segments_mut().map_err(|_| {
                CallError::<EnrichmentsError>::protocol("base URL cannot contain path segments")
            })?;
            path.pop_if_empty();
            path.push("api");
            path.push("enrichments");
            let segment = enrichment_id.to_string();
            if matches!(segment.as_str(), "." | "..") {
                return Err(CallError::<EnrichmentsError>::protocol("dot path segment"));
            }
            path.push(&segment);
            path.push("versions");
            let segment = version_id.to_string();
            if matches!(segment.as_str(), "." | "..") {
                return Err(CallError::<EnrichmentsError>::protocol("dot path segment"));
            }
            path.push(&segment);
        }
        let builder = self.http.get(url).header("accept", "application/json");
        let builder = builder.header("authorization", auth_header.as_str());
        let response = self.send(builder, "application/json").await?;
        let status = response.status().as_u16();
        let bytes = bounded_body(response, 8 * 1024 * 1024).await?;
        serde_json::from_slice(&bytes)
            .map_err(|_| CallError::protocol_response(status, "invalid JSON response"))
    }
    /// Obtain short-lived access to the Enrichment storage.
    pub async fn storage_access_http(
        &self,
        auth_header: AuthHeader,
        enrichment_id: Uuid,
        request: StorageAccessRequest,
    ) -> Result<StorageAccessResponse, CallError<EnrichmentsError>> {
        let mut url = self.base.clone();
        {
            let mut path = url.path_segments_mut().map_err(|_| {
                CallError::<EnrichmentsError>::protocol("base URL cannot contain path segments")
            })?;
            path.pop_if_empty();
            path.push("api");
            path.push("enrichments");
            let segment = enrichment_id.to_string();
            if matches!(segment.as_str(), "." | "..") {
                return Err(CallError::<EnrichmentsError>::protocol("dot path segment"));
            }
            path.push(&segment);
            path.push("storage-access");
        }
        let builder = self.http.post(url).header("accept", "application/json");
        let builder = builder.header("authorization", auth_header.as_str());
        let builder = builder
            .header("content-type", "application/json")
            .json(&request);
        let response = self.send(builder, "application/json").await?;
        let status = response.status().as_u16();
        let bytes = bounded_body(response, 8 * 1024 * 1024).await?;
        serde_json::from_slice(&bytes)
            .map_err(|_| CallError::protocol_response(status, "invalid JSON response"))
    }
    /// Obtain renewable typed storage access for the Enrichment.
    pub async fn storage_binding_http(
        &self,
        auth_header: AuthHeader,
        enrichment_id: Uuid,
        request: StorageAccessRequest,
    ) -> Result<StorageBinding, CallError<EnrichmentsError>> {
        let mut url = self.base.clone();
        {
            let mut path = url.path_segments_mut().map_err(|_| {
                CallError::<EnrichmentsError>::protocol("base URL cannot contain path segments")
            })?;
            path.pop_if_empty();
            path.push("api");
            path.push("enrichments");
            let segment = enrichment_id.to_string();
            if matches!(segment.as_str(), "." | "..") {
                return Err(CallError::<EnrichmentsError>::protocol("dot path segment"));
            }
            path.push(&segment);
            path.push("storage-binding");
        }
        let builder = self.http.post(url).header("accept", "application/json");
        let builder = builder.header("authorization", auth_header.as_str());
        let builder = builder
            .header("content-type", "application/json")
            .json(&request);
        let response = self.send(builder, "application/json").await?;
        let status = response.status().as_u16();
        let bytes = bounded_body(response, 8 * 1024 * 1024).await?;
        serde_json::from_slice(&bytes)
            .map_err(|_| CallError::protocol_response(status, "invalid JSON response"))
    }
    async fn send(
        &self,
        builder: reqwest_middleware::RequestBuilder,
        expected_media: &str,
    ) -> Result<reqwest::Response, CallError<EnrichmentsError>> {
        let response = builder
            .timeout(std::time::Duration::from_secs(60))
            .send()
            .await
            .map_err(CallError::transport)?;
        let status = response.status();
        if !status.is_success() {
            let status = status.as_u16();
            let content_type = response
                .headers()
                .get("content-type")
                .and_then(|value| value.to_str().ok())
                .map(str::to_owned);
            let (bytes, truncated) = bounded_error_body::<EnrichmentsError>(
                response,
                64 * 1024,
                status,
                content_type.clone(),
            )
            .await?;
            if truncated {
                return Err(CallError::unknown_remote(status, content_type, bytes, true));
            }
            return match serde_json::from_slice::<ErrorEnvelope>(&bytes) {
                Ok(envelope) => Err(CallError::Service(ServiceFailure::remote(status, envelope))),
                Err(_) => Err(CallError::unknown_remote(
                    status,
                    content_type,
                    bytes,
                    false,
                )),
            };
        }
        if !media_matches(&response, expected_media) {
            return Err(CallError::protocol_response(
                status.as_u16(),
                "unexpected response media type",
            ));
        }
        Ok(response)
    }
}

fn media_matches(response: &reqwest::Response, expected: &str) -> bool {
    response
        .headers()
        .get("content-type")
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.split(';').next())
        .is_some_and(|value| value.trim().eq_ignore_ascii_case(expected))
}

async fn bounded_body(
    mut response: reqwest::Response,
    limit: usize,
) -> Result<Vec<u8>, CallError<EnrichmentsError>> {
    let status = response.status().as_u16();
    let mut result = Vec::new();
    while let Some(chunk) = response
        .chunk()
        .await
        .map_err(|error| CallError::transport_response(status, error))?
    {
        if chunk.len() > limit.saturating_sub(result.len()) {
            return Err(CallError::protocol_response(
                status,
                "response exceeds JSON size limit",
            ));
        }
        result.extend_from_slice(&chunk);
    }
    Ok(result)
}

async fn bounded_error_body<E>(
    mut response: reqwest::Response,
    limit: usize,
    status: u16,
    content_type: Option<String>,
) -> Result<(Vec<u8>, bool), CallError<E>> {
    let mut result = Vec::new();
    loop {
        let chunk = response.chunk().await.map_err(|error| {
            CallError::unreadable_remote(status, content_type.clone(), result.clone(), error)
        })?;
        let Some(chunk) = chunk else {
            return Ok((result, false));
        };
        let remaining = limit.saturating_sub(result.len());
        if chunk.len() > remaining {
            result.extend_from_slice(&chunk[..remaining]);
            return Ok((result, true));
        }
        result.extend_from_slice(&chunk);
    }
}
#[async_trait]
impl EnrichmentsApi for EnrichmentsClient {
    async fn create_enrichment(
        &self,
        auth_header: AuthHeader,
        request: CreateEnrichmentRequest,
    ) -> Result<CreateEnrichmentResponse, CallError<EnrichmentsError>> {
        let response = Self::create_enrichment_http(self, auth_header, request).await?;
        Ok(response)
    }
    async fn create_version(
        &self,
        auth_header: AuthHeader,
        enrichment_id: Uuid,
        request: CreateVersionRequest,
    ) -> Result<CreateVersionResponse, CallError<EnrichmentsError>> {
        let response = Self::create_version_http(self, auth_header, enrichment_id, request).await?;
        Ok(response)
    }
    async fn read_version(
        &self,
        auth_header: AuthHeader,
        enrichment_id: Uuid,
        version_id: Uuid,
    ) -> Result<ReadVersionResponse, CallError<EnrichmentsError>> {
        let response =
            Self::read_version_http(self, auth_header, enrichment_id, version_id).await?;
        Ok(response)
    }
    async fn storage_access(
        &self,
        auth_header: AuthHeader,
        enrichment_id: Uuid,
        request: StorageAccessRequest,
    ) -> Result<StorageAccessResponse, CallError<EnrichmentsError>> {
        let response = Self::storage_access_http(self, auth_header, enrichment_id, request).await?;
        Ok(response)
    }
    async fn storage_binding(
        &self,
        auth_header: AuthHeader,
        enrichment_id: Uuid,
        request: StorageAccessRequest,
    ) -> Result<StorageBinding, CallError<EnrichmentsError>> {
        let response =
            Self::storage_binding_http(self, auth_header, enrichment_id, request).await?;
        Ok(response)
    }
}