invoist 1.0.1

Official Rust client for the Invoist API: extract invoices, read a household's bills, verify webhooks.
Documentation
//! A synchronous client, for programs without an async runtime.
//!
//! [`Client`] wraps the async [`crate::Client`] and a small single-threaded
//! Tokio runtime of its own; every method blocks until the call finishes.
//! Behaviour (retries, errors, timeouts) is identical.
//!
//! Don't call it from inside an async runtime: blocking a runtime's thread
//! panics. Async code should use [`crate::Client`] directly.
//!
//! ```no_run
//! use invoist::blocking::Client;
//! use invoist::ListBillsParams;
//!
//! let client = Client::new(std::env::var("INVOIST_API_KEY").unwrap_or_default())?;
//! let unpaid = client.bills().list(ListBillsParams::new().paid(false))?;
//! println!("{} bills to pay", unpaid.len());
//! # Ok::<(), invoist::Error>(())
//! ```

use std::future::Future;
use std::sync::Arc;

use crate::error::{Error, Result};
use crate::files::{FileDownload, UploadFile};
use crate::models::{
    BatchResult, Bill, BillDetail, BillRecord, BillsStats, BillsSummary, Category, DocumentDetail,
    DocumentListItem, ExtractResult, Extraction, HeldEmail, ReviewQueueItem, ReviewQueueListItem,
    StatsOverview, Usage, WebhookEndpointList, WebhookEndpointWithSecret,
};
use crate::params::{
    BillsStatsParams, BillsSummaryParams, CreateWebhookEndpoint, ExtractOptions, ListBillsParams,
    ListDocumentsParams, ListReviewQueueParams, ReviewResolution, WaitOptions,
};
use crate::{ClientBuilder, ExtractedDocument};

/// The synchronous Invoist client. Cheap to clone; safe to share between
/// threads. Each method is the blocking twin of the [`crate::Client`] method
/// of the same name.
#[derive(Debug, Clone)]
pub struct Client {
    inner: crate::Client,
    runtime: Arc<tokio::runtime::Runtime>,
}

impl ClientBuilder {
    /// Builds a [`blocking::Client`](Client) instead of an async one.
    pub fn build_blocking(self) -> Result<Client> {
        Client::with_client(self.build()?)
    }
}

impl Client {
    /// A client with the given API key and every other setting at its
    /// default. Fails when the key is empty.
    pub fn new(api_key: impl Into<String>) -> Result<Self> {
        Client::with_client(crate::Client::new(api_key)?)
    }

    /// A client configured from `INVOIST_API_KEY` and `INVOIST_BASE_URL`.
    pub fn from_env() -> Result<Self> {
        Client::with_client(crate::Client::from_env()?)
    }

    /// Wraps an async client, for its configuration.
    pub fn with_client(client: crate::Client) -> Result<Self> {
        let runtime = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .map_err(|e| {
                Error::invalid_argument(format!(
                    "Could not start the blocking client's runtime: {e}"
                ))
                .with_source(e)
            })?;
        Ok(Client {
            inner: client,
            runtime: Arc::new(runtime),
        })
    }

    /// The async client underneath.
    pub fn as_async(&self) -> &crate::Client {
        &self.inner
    }

    fn run<F: Future>(&self, future: F) -> F::Output {
        self.runtime.block_on(future)
    }

    /// Submits a document for extraction. See [`crate::Client::extract`].
    pub fn extract(&self, file: UploadFile, options: ExtractOptions) -> Result<ExtractResult> {
        self.run(self.inner.extract(file, options))
    }

    /// Submits up to ten documents. See [`crate::Client::extract_batch`].
    pub fn extract_batch(
        &self,
        files: impl IntoIterator<Item = UploadFile>,
    ) -> Result<BatchResult> {
        self.run(self.inner.extract_batch(files))
    }

    /// Lists documents. See [`crate::Client::list_documents`].
    pub fn list_documents(&self, params: ListDocumentsParams) -> Result<Vec<DocumentListItem>> {
        self.run(self.inner.list_documents(params))
    }

    /// Fetches a document. See [`crate::Client::get_document`].
    pub fn get_document(&self, id: &str) -> Result<DocumentDetail> {
        self.run(self.inner.get_document(id))
    }

    /// Downloads a document's file. See [`crate::Client::get_document_file`].
    pub fn get_document_file(&self, id: &str) -> Result<FileDownload> {
        self.run(self.inner.get_document_file(id))
    }

    /// Waits for an extraction to finish. See
    /// [`crate::Client::wait_for_extraction`].
    pub fn wait_for_extraction(
        &self,
        document_id: &str,
        options: WaitOptions,
    ) -> Result<Extraction> {
        self.run(self.inner.wait_for_extraction(document_id, options))
    }

    /// Submits a document and waits for it. See
    /// [`crate::Client::extract_and_wait`].
    pub fn extract_and_wait(
        &self,
        file: UploadFile,
        options: WaitOptions,
    ) -> Result<ExtractedDocument> {
        self.run(self.inner.extract_and_wait(file, options))
    }

    /// Lists review items. See [`crate::Client::list_review_queue`].
    pub fn list_review_queue(
        &self,
        params: ListReviewQueueParams,
    ) -> Result<Vec<ReviewQueueListItem>> {
        self.run(self.inner.list_review_queue(params))
    }

    /// Resolves or dismisses a review item. See
    /// [`crate::Client::resolve_review_item`].
    pub fn resolve_review_item(
        &self,
        id: &str,
        resolution: ReviewResolution,
    ) -> Result<ReviewQueueItem> {
        self.run(self.inner.resolve_review_item(id, resolution))
    }

    /// Lists spend categories. See [`crate::Client::list_categories`].
    pub fn list_categories(&self) -> Result<Vec<Category>> {
        self.run(self.inner.list_categories())
    }

    /// Usage this billing period. See [`crate::Client::get_usage`].
    pub fn get_usage(&self) -> Result<Usage> {
        self.run(self.inner.get_usage())
    }

    /// Workspace statistics. See [`crate::Client::get_stats`].
    pub fn get_stats(&self) -> Result<StatsOverview> {
        self.run(self.inner.get_stats())
    }

    /// Lists webhook endpoints. See [`crate::Client::list_webhook_endpoints`].
    pub fn list_webhook_endpoints(&self) -> Result<WebhookEndpointList> {
        self.run(self.inner.list_webhook_endpoints())
    }

    /// Registers a webhook endpoint. See
    /// [`crate::Client::create_webhook_endpoint`].
    pub fn create_webhook_endpoint(
        &self,
        endpoint: CreateWebhookEndpoint,
    ) -> Result<WebhookEndpointWithSecret> {
        self.run(self.inner.create_webhook_endpoint(endpoint))
    }

    /// Removes a webhook endpoint. See
    /// [`crate::Client::delete_webhook_endpoint`].
    pub fn delete_webhook_endpoint(&self, id: &str) -> Result<()> {
        self.run(self.inner.delete_webhook_endpoint(id))
    }

    /// A household's bills. See [`crate::Client::bills`].
    pub fn bills(&self) -> Bills<'_> {
        Bills { client: self }
    }

    /// A household's held emails. See [`crate::Client::held_emails`].
    pub fn held_emails(&self) -> HeldEmails<'_> {
        HeldEmails { client: self }
    }
}

/// A household's bills, blocking. See [`crate::Bills`].
#[derive(Debug, Clone, Copy)]
pub struct Bills<'a> {
    client: &'a Client,
}

impl Bills<'_> {
    /// Lists bills. See [`crate::Bills::list`].
    pub fn list(&self, params: ListBillsParams) -> Result<Vec<Bill>> {
        self.client.run(self.client.inner.bills().list(params))
    }

    /// Fetches a bill. See [`crate::Bills::get`].
    pub fn get(&self, id: &str) -> Result<BillDetail> {
        self.client.run(self.client.inner.bills().get(id))
    }

    /// Downloads a bill's file. See [`crate::Bills::get_file`].
    pub fn get_file(&self, id: &str) -> Result<FileDownload> {
        self.client.run(self.client.inner.bills().get_file(id))
    }

    /// What there is to pay. See [`crate::Bills::summary`].
    pub fn summary(&self, params: BillsSummaryParams) -> Result<BillsSummary> {
        self.client.run(self.client.inner.bills().summary(params))
    }

    /// A month at a glance. See [`crate::Bills::stats`].
    pub fn stats(&self, params: BillsStatsParams) -> Result<BillsStats> {
        self.client.run(self.client.inner.bills().stats(params))
    }

    /// Marks a bill paid. See [`crate::Bills::mark_paid`].
    pub fn mark_paid(&self, id: &str) -> Result<BillRecord> {
        self.client.run(self.client.inner.bills().mark_paid(id))
    }

    /// Marks a bill not paid. See [`crate::Bills::mark_unpaid`].
    pub fn mark_unpaid(&self, id: &str) -> Result<BillRecord> {
        self.client.run(self.client.inner.bills().mark_unpaid(id))
    }
}

/// A household's held emails, blocking. See [`crate::HeldEmails`].
#[derive(Debug, Clone, Copy)]
pub struct HeldEmails<'a> {
    client: &'a Client,
}

impl HeldEmails<'_> {
    /// Lists held emails. See [`crate::HeldEmails::list`].
    pub fn list(&self) -> Result<Vec<HeldEmail>> {
        self.client.run(self.client.inner.held_emails().list())
    }
}