spiral-sdk 0.1.11

Rust SDK for Spiral resource and table APIs
Documentation
//! Credential-specific application handles and organization administration.

use crate::auth::{CredentialChain, OidcAuthProvider, TokenSource, token_endpoint};
use crate::resources::applications::{
    Application as ApplicationInfo, ApplicationConfiguration, ApplicationsClient,
    Installation as InstallationInfo, InstallationDetails, InstallationStatusChange,
    InstallationSummary, InstallationsClient, ListApplicationsRequest,
};
use crate::{Client, Error, List, Page, Project};

/// The authenticated application. Registration administration is a separate handle.
///
/// ```compile_fail
/// fn delete(app: spiral::Application) { app.delete(); }
/// ```
#[derive(Clone)]
pub struct Application {
    client: ApplicationsClient,
}

/// A project-owned application registration, accessed with management credentials.
#[derive(Clone)]
pub struct ApplicationRegistration {
    client: ApplicationsClient,
    id: String,
}

/// An installation discovered by its owning application, before authenticating as it.
///
/// ```compile_fail
/// fn projects(installation: spiral::ApplicationInstallation) { installation.projects(); }
/// ```
#[derive(Clone)]
pub struct ApplicationInstallation {
    client: ApplicationsClient,
    id: String,
}

/// An authenticated installation. Project operations retain installation credentials.
#[derive(Clone)]
pub struct Installation {
    client: InstallationsClient,
}

/// An organization installation accessed with organization management credentials.
#[derive(Clone)]
pub struct InstallationRegistration {
    client: InstallationsClient,
    id: String,
}

impl Client {
    /// Select an application registration for management, without a request.
    pub fn application(&self, id: impl Into<String>) -> ApplicationRegistration {
        self.applications().application(id)
    }
    /// Select an organization installation for management, without a request.
    pub fn installation(&self, id: impl Into<String>) -> InstallationRegistration {
        self.installations().installation(id)
    }
}
impl Application {
    pub(crate) fn new(client: ApplicationsClient) -> Self {
        Self { client }
    }
    /// Read the authenticated application's metadata.
    pub async fn get(&self) -> Result<ApplicationInfo, Error> {
        Ok(self.client.service.client.get_self().await?)
    }
    /// Discover installations as handles that can obtain installation credentials.
    pub fn installations(&self) -> List<'_, ApplicationInstallation> {
        List::new(move |options| {
            Box::pin(async move {
                let page = self
                    .client
                    .service
                    .client
                    .list_self_installations(ListApplicationsRequest {
                        page_size: options.size,
                        page_token: options.token,
                    })
                    .await?;
                Ok(Page {
                    items: page
                        .installations
                        .into_iter()
                        .map(|info| self.installation(info.id))
                        .collect(),
                    next_page_token: page.next_page_token,
                })
            })
        })
    }
    /// Select an installation of this application. This does not change credentials.
    pub fn installation(&self, id: impl Into<String>) -> ApplicationInstallation {
        ApplicationInstallation {
            client: self.client.clone(),
            id: id.into(),
        }
    }
}
impl ApplicationInstallation {
    /// Stable installation identifier.
    pub fn id(&self) -> &str {
        &self.id
    }
    /// Read installation metadata using app authentication.
    pub async fn get(&self) -> Result<InstallationDetails, Error> {
        Ok(self
            .client
            .service
            .client
            .get_self_installation(self.id.clone())
            .await?)
    }
    /// Authenticate as this installation using the app's renewable credentials.
    /// The returned handle retains the API, data and auth endpoints and embedded library
    /// configuration. Embedded execution obtains storage credentials as the installation.
    /// Custom backends cannot be rebound and return an error instead of changing execution.
    /// The authorization server checks the app binding and active installation registration.
    pub async fn authenticate(&self) -> Result<Installation, Error> {
        let service = &self.client.service;
        let source = service.auth.clone().ok_or(Error::CredentialsNotFound)?;
        let provider = TokenSource::from(CredentialChain::new(vec![
            OidcAuthProvider::new(
                service.http.clone(),
                token_endpoint(&service.auth_url)?,
                self.id.clone(),
            )
            .with_assertion_provider(source)
            .into(),
        ]));
        let client = service.connection(Some(provider))?;
        // Fail at the explicit transition, before returning a usable installation handle.
        client.access_token().await?;
        Ok(client.installations().current())
    }
}
impl ApplicationRegistration {
    pub(crate) fn new(client: ApplicationsClient, id: String) -> Self {
        Self { client, id }
    }
    /// Stable registration identifier.
    pub fn id(&self) -> &str {
        &self.id
    }
    /// Read registration metadata.
    pub async fn get(&self) -> Result<ApplicationInfo, Error> {
        Ok(self
            .client
            .service
            .client
            .get_application(self.id.clone())
            .await?)
    }
    /// Replace registration configuration with registration management permission.
    pub async fn update(
        &self,
        configuration: ApplicationConfiguration,
    ) -> Result<ApplicationInfo, Error> {
        Ok(self
            .client
            .service
            .client
            .put_application(self.id.clone(), configuration)
            .await?)
    }
    /// Delete the registration and uninstall it everywhere.
    pub async fn delete(&self) -> Result<ApplicationInfo, Error> {
        Ok(self
            .client
            .service
            .client
            .delete_application(self.id.clone())
            .await?)
    }
    /// Inspect this registration's installations with developer management permission.
    pub fn installations(&self) -> List<'_, InstallationInfo> {
        self.client
            .service
            .client
            .list_application_installations_list(self.id.clone(), Default::default())
    }
}
impl Installation {
    pub(crate) fn current(client: InstallationsClient) -> Self {
        Self { client }
    }
    /// Read the credential-derived installation's live state.
    pub async fn get(&self) -> Result<InstallationDetails, Error> {
        Ok(self
            .client
            .service
            .client
            .get_current_installation()
            .await?)
    }
    /// List projects authorized for this installation. Every page rechecks access.
    pub fn projects(&self) -> List<'_, Project> {
        List::new(move |options| {
            Box::pin(async move {
                let service = &self.client.service;
                let page = service
                    .client
                    .list_installation_projects(ListApplicationsRequest {
                        page_size: options.size,
                        page_token: options.token,
                    })
                    .await?;
                let client = &self.client.connection;
                Ok(Page {
                    items: page
                        .projects
                        .into_iter()
                        .map(|info| client.project(info.id))
                        .collect::<Result<_, _>>()?,
                    next_page_token: page.next_page_token,
                })
            })
        })
    }
    /// Select a project with installation credentials; server authorization applies to every
    /// request.
    pub fn project(&self, id: impl AsRef<str>) -> Result<Project, Error> {
        self.client.connection.project(id)
    }
}
impl InstallationRegistration {
    pub(crate) fn new(client: InstallationsClient, id: String) -> Self {
        Self { client, id }
    }
    /// Stable installation identifier.
    pub fn id(&self) -> &str {
        &self.id
    }
    /// Read the organization's installation configuration.
    pub async fn get(&self) -> Result<InstallationInfo, Error> {
        Ok(self
            .client
            .service
            .client
            .get_installation(self.id.clone())
            .await?)
    }
    /// Approve the latest request as an organization owner.
    pub async fn approve(&self) -> Result<InstallationInfo, Error> {
        Ok(self
            .client
            .service
            .client
            .approve_installation(self.id.clone())
            .await?)
    }
    /// Suspend an installation as an organization owner.
    pub async fn suspend(&self) -> Result<InstallationInfo, Error> {
        Ok(self
            .client
            .service
            .client
            .set_installation_status(self.id.clone(), InstallationStatusChange { enabled: false })
            .await?)
    }
    /// Resume an installation as an organization owner.
    pub async fn resume(&self) -> Result<InstallationInfo, Error> {
        Ok(self
            .client
            .service
            .client
            .set_installation_status(self.id.clone(), InstallationStatusChange { enabled: true })
            .await?)
    }
    /// Uninstall the app as an organization owner.
    pub async fn uninstall(&self) -> Result<InstallationInfo, Error> {
        Ok(self
            .client
            .service
            .client
            .uninstall(self.id.clone())
            .await?)
    }
}

/// The same installation viewed through delegated user credentials, never a machine token.
///
/// ```compile_fail
/// fn approve(installation: spiral::UserInstallation) { installation.approve(); }
/// ```
#[derive(Clone)]
pub struct UserInstallation {
    client: InstallationsClient,
    info: InstallationSummary,
}
impl UserInstallation {
    pub(crate) fn new(client: InstallationsClient, info: InstallationSummary) -> Self {
        Self { client, info }
    }
    /// Stable installation identifier.
    pub fn id(&self) -> &str {
        &self.info.id
    }
    /// Metadata returned during discovery, without another request.
    pub fn info(&self) -> &InstallationSummary {
        &self.info
    }
    /// Refresh installation metadata, retaining the original installation identity.
    pub async fn get(&self) -> Result<InstallationSummary, Error> {
        let service = &self.client.service;
        service
            .client
            .get_user_installation()
            .await?
            .installation
            .filter(|info| info.id == self.info.id)
            .ok_or_else(|| Error::NotFound(self.info.id.clone()))
    }
    /// List project metadata in the user's intersection with this installation.
    pub fn projects(&self) -> List<'_, Project> {
        List::new(move |options| {
            Box::pin(async move {
                let service = &self.client.service;
                let page = service
                    .client
                    .list_user_installation_projects(
                        self.info.id.clone(),
                        ListApplicationsRequest {
                            page_size: options.size,
                            page_token: options.token,
                        },
                    )
                    .await?;
                Ok(Page {
                    items: page
                        .projects
                        .into_iter()
                        .map(|info| self.project(info.id))
                        .collect::<Result<_, _>>()?,
                    next_page_token: page.next_page_token,
                })
            })
        })
    }
    /// Select a project using this delegated user credential. The server enforces the live
    /// user and installation intersection on every project and resource authorization check.
    pub fn project(&self, id: impl AsRef<str>) -> Result<Project, Error> {
        self.client.connection.project(id)
    }
}