spiral-sdk 0.1.10

Rust SDK for Spiral resource and table APIs
Documentation
//! Table and Enrichment resource operations over generated HTTP contracts.

use std::fmt::{Debug, Formatter};
use std::time::Duration;

use reqwest::Url;
use reqwest::redirect::Policy;
use spiral_contract::call::CallError;
use uuid::Uuid;

use crate::Error;
use crate::api::enrichments::{
    CreateEnrichmentRequest, CreateEnrichmentResponse,
    CreateVersionRequest as CreateEnrichmentVersionRequest,
    CreateVersionResponse as CreateEnrichmentVersionResponse, EnrichmentsApi, EnrichmentsClient,
    ReadVersionResponse as ReadEnrichmentVersionResponse,
    StorageAccessRequest as EnrichmentStorageAccessRequest,
    StorageAccessResponse as EnrichmentStorageAccessResponse,
    StorageBinding as EnrichmentStorageBinding,
};
use crate::api::tables::objects::TableId;
use crate::api::tables::{
    CreateTableRequest, CreateTableResponse, CreateVersionRequest, CreateVersionResponse,
    ReadVersionResponse, StorageAccessRequest, StorageAccessResponse, StorageBinding, TablesApi,
    TablesClient,
};
use crate::auth::TokenSource;
use crate::http::{HttpClient, http_client_builder};

/// Authenticated Table and Enrichment operations sharing one transport and identity.
#[derive(Clone)]
pub struct Client {
    http: HttpClient,
    tables: TablesClient,
    enrichments: EnrichmentsClient,
}

impl Debug for Client {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("Client")
            .field("base_url", self.http.base_url())
            .finish_non_exhaustive()
    }
}

impl Client {
    /// Create a Table resource client with bounded requests and no credential provider.
    pub fn new(base_url: &str) -> Result<Self, Error> {
        let base = Url::parse(base_url).map_err(Error::invalid)?;
        let client = http_client_builder()
            .gzip(true)
            .redirect(Policy::none())
            .timeout(Duration::from_secs(60))
            .build()
            .map_err(Error::invalid)?;
        Self::from_http(HttpClient::new(base, client, None))
    }

    pub(crate) fn from_http(http: HttpClient) -> Result<Self, Error> {
        let tables =
            TablesClient::from_transport(http.base_url().as_str(), http.middleware_client())
                .map_err(Error::from)?;
        let enrichments =
            EnrichmentsClient::from_transport(http.base_url().as_str(), http.middleware_client())
                .map_err(Error::from)?;
        Ok(Self {
            http,
            tables,
            enrichments,
        })
    }

    /// Reuse the supplied redirect-disabled connection pool.
    pub fn with_client(mut self, client: reqwest::Client) -> Self {
        self.http = self.http.with_client(client);
        self.tables = self.tables.with_transport(self.http.middleware_client());
        self.enrichments = self
            .enrichments
            .with_transport(self.http.middleware_client());
        self
    }

    /// Resolve the current credential before every generated service call.
    pub fn with_token_source(mut self, provider: TokenSource) -> Self {
        self.http = self.http.with_token_source(provider);
        self
    }

    /// Allocate a Table and its storage before publishing its first version.
    pub async fn create(&self, request: &CreateTableRequest) -> Result<CreateTableResponse, Error> {
        self.tables
            .create_table(self.http.auth_header().await?, request.clone())
            .await
            .map_err(Error::from)
    }

    /// Obtain scoped SPFS access to a Table's storage.
    pub async fn storage_access(
        &self,
        table_id: TableId,
        request: &StorageAccessRequest,
    ) -> Result<StorageAccessResponse, Error> {
        self.tables
            .storage_access(self.http.auth_header().await?, table_id, request.clone())
            .await
            .map_err(Error::from)
    }

    /// Obtain typed storage access, retaining the absent-binding fallback on HTTP 404.
    pub async fn storage_binding(
        &self,
        table_id: TableId,
        request: &StorageAccessRequest,
    ) -> Result<Option<StorageBinding>, Error> {
        match self
            .tables
            .storage_binding(self.http.auth_header().await?, table_id, request.clone())
            .await
        {
            Ok(binding) => Ok(Some(binding)),
            Err(error) if missing(&error) => Ok(None),
            Err(error) => Err(error.into()),
        }
    }

    /// Register one immutable Table version prepared by the execution backend.
    pub async fn create_version(
        &self,
        table_id: TableId,
        request: &CreateVersionRequest,
    ) -> Result<CreateVersionResponse, Error> {
        self.tables
            .create_version(self.http.auth_header().await?, table_id, request.clone())
            .await
            .map_err(Error::from)
    }

    /// Read the exact Table version's storage root and ancestry.
    pub async fn get_version(
        &self,
        table_id: TableId,
        version_id: Uuid,
    ) -> Result<ReadVersionResponse, Error> {
        self.tables
            .read_version(self.http.auth_header().await?, table_id, version_id)
            .await
            .map_err(Error::from)
    }

    /// Allocate an Enrichment root and backing storage for a source Table.
    pub async fn create_enrichment(
        &self,
        request: &CreateEnrichmentRequest,
    ) -> Result<CreateEnrichmentResponse, Error> {
        self.enrichments
            .create_enrichment(self.http.auth_header().await?, request.clone())
            .await
            .map_err(Error::from)
    }

    /// Obtain scoped SPFS access to an Enrichment's storage.
    pub async fn enrichment_storage_access(
        &self,
        enrichment_id: Uuid,
        request: &EnrichmentStorageAccessRequest,
    ) -> Result<EnrichmentStorageAccessResponse, Error> {
        self.enrichments
            .storage_access(
                self.http.auth_header().await?,
                enrichment_id,
                request.clone(),
            )
            .await
            .map_err(Error::from)
    }

    /// Obtain typed Enrichment storage access, retaining the absent-binding fallback on HTTP 404.
    pub async fn enrichment_storage_binding(
        &self,
        enrichment_id: Uuid,
        request: &EnrichmentStorageAccessRequest,
    ) -> Result<Option<EnrichmentStorageBinding>, Error> {
        match self
            .enrichments
            .storage_binding(
                self.http.auth_header().await?,
                enrichment_id,
                request.clone(),
            )
            .await
        {
            Ok(binding) => Ok(Some(binding)),
            Err(error) if missing(&error) => Ok(None),
            Err(error) => Err(error.into()),
        }
    }

    /// Register an immutable Enrichment successor of the supplied base.
    pub async fn create_enrichment_version(
        &self,
        enrichment_id: Uuid,
        request: &CreateEnrichmentVersionRequest,
    ) -> Result<CreateEnrichmentVersionResponse, Error> {
        self.enrichments
            .create_version(
                self.http.auth_header().await?,
                enrichment_id,
                request.clone(),
            )
            .await
            .map_err(Error::from)
    }

    /// Read an Enrichment root and one exact immutable version.
    pub async fn read_enrichment_version(
        &self,
        enrichment_id: Uuid,
        version_id: Uuid,
    ) -> Result<ReadEnrichmentVersionResponse, Error> {
        self.enrichments
            .read_version(self.http.auth_header().await?, enrichment_id, version_id)
            .await
            .map_err(Error::from)
    }
}

fn missing<E>(error: &CallError<E>) -> bool {
    match error {
        CallError::Service(failure) => failure.status() == 404,
        CallError::UnknownRemote(failure) => failure.status() == 404,
        _ => false,
    }
}