mongor 0.1.10

Ergonomic MongoDB ODM
Documentation
// Authors: Robert Lopez
// License: MIT (See `LICENSE.md`)

use mongodb::{bson::oid::ObjectId, options::DropDatabaseOptions, Client, Collection, Database};
use serde::{Deserialize, Serialize};
use std::future::Future;

pub mod assert_document;
pub mod assert_model;
pub mod test_error;

/// A wrapper around `Database`, associated with a `ObjectId` as a `id`.
///
/// The purpose of this structure is to create a isolated database,
/// and then to run the test code via `run_test`, which will take
/// ownership of the TestDB struct, and execute the `dropDatabase`
/// command to clean up the database. Providing a more ergonomic
/// way to run a async test suite.
///
/// The test closure expects to return `Ok(())` if the test passed,
/// or a `Err(dyn std::error::Error)` on failure. A macro `test_error!` is
/// provided to more ergonomically exit the test, similar to using `panic!`.
///
/// ---
/// Example Usage:
/// ```
///
/// let client: Client = ...;
///
/// let test_db = TestDB::new(&client);
///
/// let collection: Collection<D> = test_db.db.collection("collection");
///
/// let model: Model<D> = Model::from(collection);
///
/// test_db.run_test(
///     || async {
///         ...
///
///         let document: D = model.find_one_by_field_value(
///             "username",
///             "admin",
///             None,
///             None,
///         ).await?;
///
///         if document.total_posts != 1337 {
///             // Ends test, by calling `Err(TestError(message))?`
///             test_error!(
///                 "Document: {:?} should of had 1337 `total_posts`"
///                 document,
///             );
///         }
///
///         Ok(())
///     },
///     None,
/// ).await.unwrap();
/// ```
pub struct TestDB {
    pub id: String,
    pub db: Database,
}

impl TestDB {
    /// Constructs a new `TestDB`, using a new ObjectId as
    /// the `id` field.
    ///
    /// ---
    /// Example Usage:
    /// ```
    ///
    /// let client: Client = ...;
    ///
    /// let db = TestDB::new(&client);
    /// ```
    pub fn new(client: &Client) -> Self {
        let id = ObjectId::new().to_string();
        let db = client.database(&id);

        Self { id, db }
    }

    /// Executes a `dropDatabase` command.
    ///
    /// *PANICS*: If `mongodb::Database::drop` fails.
    ///
    /// ---
    /// Example Usage:
    /// ```
    ///
    /// let db = TestDB::new(&client);
    ///
    /// db.drop(None).await;
    /// ```
    pub async fn drop(&self, options: Option<DropDatabaseOptions>) {
        self.db
            .drop()
            .with_options(options)
            .await
            .unwrap_or_else(|_| {
                panic!(
                    "Could not drop TestDB with ID: {}, please manually drop this db to clear the space.",
                    self.id
                )
            });
    }

    /// Construct a `Collection<D>` from a collection `name`
    ///
    /// ---
    /// Example Usage:
    /// ```
    ///
    /// #[derive(Serialize, Deserialize)]
    /// pub struct Shark { ... };
    ///
    /// let db = TestDB::new(&client);
    /// let shark_collection: Collection<Shark> = db.collection("shark");
    /// ```
    pub fn collection<D>(&self, name: &str) -> Collection<D>
    where
        D: Serialize + for<'a> Deserialize<'a> + Send + Sync + Unpin,
    {
        self.db.collection(name)
    }

    /// Runs a isolated test, dropping the database on failure or success,
    /// and taking ownership to avoid re-using a dropped database.
    ///
    /// The test closure expects to return `Ok(())` if the test passed,
    /// or a `Err(dyn std::error::Error)` on failure. A macro `test_error!` is
    /// provided to more ergonomically exit the test, similar to using `panic!`.
    ///
    /// *PANICS*: If `mongodb::Database::drop` fails.
    ///
    /// ---
    /// Example Usage:
    /// ```
    ///
    /// let client: Client = ...;
    ///
    /// let test_db = TestDB::new(&client);
    ///
    /// let collection: Collection<D> = test_db.db.collection("collection");
    ///
    /// let model: Model<D> = Model::from(collection);
    ///
    /// test_db.run_test(
    ///     || async {
    ///         ...
    ///
    ///         let document: D = model.find_one_by_field_value(
    ///             "username",
    ///             "admin",
    ///             None,
    ///             None,
    ///         ).await?;
    ///
    ///         if document.total_posts != 1337 {
    ///             // Ends test, by calling `Err(TestError(message))?`
    ///             test_error!(
    ///                 "Document: {:?} should of had 1337 `total_posts`"
    ///                 document,
    ///             );
    ///         }
    ///
    ///         Ok(())
    ///     },
    ///     None,
    /// ).await.unwrap();
    /// ```
    pub async fn run_test<T, Fut>(
        self,
        test: T,
        drop_options: Option<DropDatabaseOptions>,
    ) -> Result<(), Box<dyn std::error::Error>>
    where
        T: FnOnce() -> Fut,
        Fut: Future<Output = Result<(), Box<dyn std::error::Error>>>,
    {
        let result = test().await;
        self.drop(drop_options).await;

        result
    }
}