ayb 0.1.12-rc.7

ayb makes it easy to create, host, and share embedded databases like SQLite and DuckDB
Documentation
use crate::ayb_db::models::{DBType, EntityDatabaseSharingLevel, EntityType, PublicSharingLevel};
use crate::error::AybError;
use crate::hosted_db::QueryResult;
use crate::http::structs::{
    APIToken, Database, DatabaseDetails, DatabasePermissions, EmptyResponse, EntityQueryResponse,
    SnapshotList, TokenList,
};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::de::DeserializeOwned;
use std::collections::HashMap;

pub struct AybClient {
    pub base_url: String,
    pub api_token: Option<String>,
}

impl AybClient {
    fn make_url(&self, endpoint: String) -> String {
        format!("{}/v1/{}", self.base_url, endpoint)
    }

    fn add_bearer_token(&self, headers: &mut HeaderMap) -> Result<(), AybError> {
        if let Some(api_token) = &self.api_token {
            headers.insert(
                HeaderName::from_static("authorization"),
                HeaderValue::from_str(format!("Bearer {api_token}").as_str()).unwrap(),
            );
            Ok(())
        } else {
            Err(AybError::Other {
                message: "Calling endpoint that requires client API token, but none provided"
                    .to_string(),
            })
        }
    }

    async fn handle_response<T: DeserializeOwned>(
        &self,
        response: reqwest::Response,
        expected_status: reqwest::StatusCode,
    ) -> Result<T, AybError> {
        let status = response.status();
        if status == expected_status {
            response.json::<T>().await.map_err(|err| AybError::Other {
                message: format!("Unable to parse successful response: {err}"),
            })
        } else {
            response
                .json::<AybError>()
                .await
                .map(|v| Err(v))
                .map_err(|error| AybError::Other {
                    message: format!(
                        "Unable to parse error response: {error:#?}, response code: {status}"
                    ),
                })?
        }
    }

    async fn handle_empty_response(
        &self,
        response: reqwest::Response,
        expected_status: reqwest::StatusCode,
    ) -> Result<(), AybError> {
        let status = response.status();
        if status == expected_status {
            Ok(())
        } else {
            response
                .json::<AybError>()
                .await
                .map(Err)
                .map_err(|error| AybError::Other {
                    message: format!(
                        "Unable to parse error response: {error:#?}, response code: {status}"
                    ),
                })?
        }
    }

    pub async fn confirm(&self, authentication_token: &str) -> Result<APIToken, AybError> {
        let mut headers = HeaderMap::new();
        headers.insert(
            HeaderName::from_static("authentication-token"),
            HeaderValue::from_str(authentication_token).unwrap(),
        );

        let response = reqwest::Client::new()
            .post(self.make_url("confirm".to_owned()))
            .headers(headers)
            .send()
            .await?;

        self.handle_response(response, reqwest::StatusCode::OK)
            .await
    }

    pub async fn create_database(
        &self,
        entity: &str,
        database: &str,
        db_type: &DBType,
        public_sharing_level: &PublicSharingLevel,
    ) -> Result<Database, AybError> {
        let mut headers = HeaderMap::new();
        headers.insert(
            HeaderName::from_static("db-type"),
            HeaderValue::from_str(db_type.to_str()).unwrap(),
        );
        headers.insert(
            HeaderName::from_static("public-sharing-level"),
            HeaderValue::from_str(public_sharing_level.to_str()).unwrap(),
        );
        self.add_bearer_token(&mut headers)?;

        let response = reqwest::Client::new()
            .post(self.make_url(format!("{entity}/{database}/create")))
            .headers(headers)
            .send()
            .await?;

        self.handle_response(response, reqwest::StatusCode::CREATED)
            .await
    }

    pub async fn list_snapshots(
        &self,
        entity: &str,
        database: &str,
    ) -> Result<SnapshotList, AybError> {
        let mut headers = HeaderMap::new();
        self.add_bearer_token(&mut headers)?;

        let response = reqwest::Client::new()
            .get(self.make_url(format!("{entity}/{database}/list_snapshots")))
            .headers(headers)
            .send()
            .await?;

        self.handle_response(response, reqwest::StatusCode::OK)
            .await
    }

    pub async fn log_in(&self, entity: &str) -> Result<EmptyResponse, AybError> {
        let mut headers = HeaderMap::new();
        headers.insert(
            HeaderName::from_static("entity"),
            HeaderValue::from_str(entity).unwrap(),
        );

        let response = reqwest::Client::new()
            .post(self.make_url("log_in".to_owned()))
            .headers(headers)
            .send()
            .await?;

        self.handle_response(response, reqwest::StatusCode::OK)
            .await
    }

    pub async fn entity_details(&self, entity: &str) -> Result<EntityQueryResponse, AybError> {
        let mut headers = HeaderMap::new();
        self.add_bearer_token(&mut headers)?;

        let response = reqwest::Client::new()
            .get(self.make_url(format!("entity/{entity}")))
            .headers(headers)
            .send()
            .await?;

        self.handle_response(response, reqwest::StatusCode::OK)
            .await
    }

    pub async fn query(
        &self,
        entity: &str,
        database: &str,
        query: &str,
    ) -> Result<QueryResult, AybError> {
        let mut headers = HeaderMap::new();
        self.add_bearer_token(&mut headers)?;

        let response = reqwest::Client::new()
            .post(self.make_url(format!("{entity}/{database}/query")))
            .headers(headers)
            .body(query.to_owned())
            .send()
            .await?;

        self.handle_response(response, reqwest::StatusCode::OK)
            .await
    }

    pub async fn register(
        &self,
        entity: &str,
        email_address: &str,
        entity_type: &EntityType,
    ) -> Result<EmptyResponse, AybError> {
        let mut headers = HeaderMap::new();
        headers.insert(
            HeaderName::from_static("entity"),
            HeaderValue::from_str(entity).unwrap(),
        );
        headers.insert(
            HeaderName::from_static("email-address"),
            HeaderValue::from_str(email_address).unwrap(),
        );
        headers.insert(
            HeaderName::from_static("entity-type"),
            HeaderValue::from_str(entity_type.to_str()).unwrap(),
        );

        let response = reqwest::Client::new()
            .post(self.make_url("register".to_string()))
            .headers(headers)
            .send()
            .await?;

        self.handle_response(response, reqwest::StatusCode::OK)
            .await
    }

    pub async fn restore_snapshot(
        &self,
        entity: &str,
        database: &str,
        snapshot_id: &str,
    ) -> Result<(), AybError> {
        let mut headers = HeaderMap::new();
        self.add_bearer_token(&mut headers)?;

        let response = reqwest::Client::new()
            .post(self.make_url(format!("{entity}/{database}/restore_snapshot")))
            .headers(headers)
            .body(snapshot_id.to_owned())
            .send()
            .await?;

        self.handle_empty_response(response, reqwest::StatusCode::OK)
            .await
    }

    pub async fn update_profile(
        &self,
        entity: &str,
        profile_update: &HashMap<String, Option<String>>,
    ) -> Result<(), AybError> {
        let mut headers = HeaderMap::new();
        self.add_bearer_token(&mut headers)?;

        headers.insert(
            "Content-Type",
            "application/json"
                .parse()
                .expect("const value must be valid"),
        );

        let response = reqwest::Client::new()
            .patch(self.make_url(format!("entity/{entity}")))
            .headers(headers)
            .body(serde_json::to_string(profile_update)?)
            .send()
            .await?;

        self.handle_empty_response(response, reqwest::StatusCode::OK)
            .await
    }

    pub async fn database_details(
        &self,
        entity: &str,
        database: &str,
    ) -> Result<DatabaseDetails, AybError> {
        let mut headers = HeaderMap::new();
        self.add_bearer_token(&mut headers)?;

        let response = reqwest::Client::new()
            .get(self.make_url(format!("{entity}/{database}/details")))
            .headers(headers)
            .send()
            .await?;

        self.handle_response(response, reqwest::StatusCode::OK)
            .await
    }

    pub async fn update_database(
        &self,
        entity: &str,
        database: &str,
        public_sharing_level: &PublicSharingLevel,
    ) -> Result<(), AybError> {
        let mut headers = HeaderMap::new();
        self.add_bearer_token(&mut headers)?;

        headers.insert(
            HeaderName::from_static("public-sharing-level"),
            HeaderValue::from_str(public_sharing_level.to_str()).unwrap(),
        );

        let response = reqwest::Client::new()
            .patch(self.make_url(format!("{entity}/{database}/update")))
            .headers(headers)
            .send()
            .await?;

        self.handle_empty_response(response, reqwest::StatusCode::OK)
            .await
    }

    pub async fn share(
        &self,
        entity_for_database: &str,
        database: &str,
        entity_for_permission: &str,
        sharing_level: &EntityDatabaseSharingLevel,
    ) -> Result<(), AybError> {
        let mut headers = HeaderMap::new();
        self.add_bearer_token(&mut headers)?;

        headers.insert(
            HeaderName::from_static("entity-for-permission"),
            HeaderValue::from_str(entity_for_permission).unwrap(),
        );

        headers.insert(
            HeaderName::from_static("sharing-level"),
            HeaderValue::from_str(sharing_level.to_str()).unwrap(),
        );

        let response = reqwest::Client::new()
            .post(self.make_url(format!("{entity_for_database}/{database}/share")))
            .headers(headers)
            .send()
            .await?;

        self.handle_empty_response(response, reqwest::StatusCode::NO_CONTENT)
            .await
    }

    pub async fn list_database_permissions(
        &self,
        entity: &str,
        database: &str,
    ) -> Result<DatabasePermissions, AybError> {
        let mut headers = HeaderMap::new();
        self.add_bearer_token(&mut headers)?;

        let response = reqwest::Client::new()
            .get(self.make_url(format!("{entity}/{database}/permissions")))
            .headers(headers)
            .send()
            .await?;

        self.handle_response(response, reqwest::StatusCode::OK)
            .await
    }

    pub async fn list_tokens(&self) -> Result<TokenList, AybError> {
        let mut headers = HeaderMap::new();
        self.add_bearer_token(&mut headers)?;

        let response = reqwest::Client::new()
            .get(self.make_url("tokens".to_owned()))
            .headers(headers)
            .send()
            .await?;

        self.handle_response(response, reqwest::StatusCode::OK)
            .await
    }

    pub async fn revoke_token(&self, short_token: &str) -> Result<(), AybError> {
        let mut headers = HeaderMap::new();
        self.add_bearer_token(&mut headers)?;

        let response = reqwest::Client::new()
            .delete(self.make_url(format!("tokens/{short_token}")))
            .headers(headers)
            .send()
            .await?;

        self.handle_empty_response(response, reqwest::StatusCode::OK)
            .await
    }
}