Macro mongo_db

Source
macro_rules! mongo_db {
    (
        $({
            $($outer_tokens:tt)+
        })?

        $(#[$additional_db_attr:meta])*
        $db_name:ident {

            $({
                $($inner_tokens:tt)+
            })?

            $(
                $(#[$additional_coll_attr:meta])*
                $coll_name:ident$(<$($collection_param_name:ident: $collection_param_value:tt),+>)? {
                    $(
                        $(#[$additional_field_attr:meta])*
                        $field:ident: $field_type:ty
                    ),*$(,)?
                }
                $(-{
                    $($inner_impl:tt)+
                })?
            );+$(;)?
        }
        $(-{
            $($outer_impl:tt)+
        })?
    ) => { ... };
}
Expand description

Model a mongodb database.

This macro creates structs / functions / constants / modules that represent a mongoDB database. Being a macro (which is expanded at compile time) there is no run time performance penalty when using this macro.

For a detailled syntax demonstration see Examples.

§Structure

This macro wraps everything in a module called mongo.

The main database handler has the following attributes:

  • Its name represents the database’s name (eg. a database named MyDatabase has a struct mongo::MyDatabase).
  • It implements the MongoClient trait.
  • It contains handles to all given collections inside the database. These handles have the format {collection_name}_coll where {collection_name} represents the collection’s name in snake_case.
  • It also contains a client and a database field for you to use.

All collections are wrapped in an additional public module named schema.

Each collection has its own struct which stores all specified fields. All collection structs implement Serialize, Deserialize and MongoCollection.

By default a field _id gets added to each collection automatically: pub _id: Option<DefaultId> (see DefaultId for more info). This field needs to exist for you to be able to obtain an _id field from the database. When serializing, _id gets skipped if it is None. All fields except _id get renamed to camelCase when serializing (converting _id to camelCase results in id).

Note: All structs’ names in camelCase can be accessed via the MongoClient / MongoCollection trait.

§Examples

§General Examples

use mongodb_ext::{mongo_db, MongoClient, MongoCollection, DefaultId};
use serde_json::ser;

mongo_db! {
    // database name
    SomeDatabase {
        // additional attributes for the collection
        #[derive(Debug, Clone)]
        // collection name
        SomeCollection {
            // collection fields
            first_name: String,
        }
    }
}

let mut some_document = mongo::schema::SomeCollection {
    _id: None,
    first_name: String::from("alice")
};

// When serializing, `_id` is skipped only if it is `None`.
// Note the key conversion to `camelCase`.
assert_eq!(
    ser::to_string(&some_document).unwrap(),
    String::from("{\"firstName\":\"alice\"}")
);

// update `_id` field to include in serialization.
let oid = DefaultId::parse_str("0123456789ABCDEF01234567").unwrap();
some_document._id = Some(oid);
assert_eq!(
    ser::to_string(&some_document).unwrap(),
    String::from("{\"_id\":{\"$oid\":\"0123456789abcdef01234567\"},\"firstName\":\"alice\"}")
);

// constants store the collection / database names in `camelCase` + collection version
assert_eq!("someCollection", mongo::schema::SomeCollection::NAME);
assert_eq!(1, mongo::schema::SomeCollection::SCHEMA_VERSION);
assert_eq!("someDatabase", mongo::SomeDatabase::NAME);

Multiple collections need to be separated by ;, a trailing ; is optional:

use mongodb_ext::{mongo_db, MongoCollection, MongoClient};

mongo_db! {
    #[derive(Debug, Clone)]
    MyDatabase {
        #[derive(Debug, Clone)]
        MyFirstCollection {
            first_name: String,
            last_name: String,
            age: u8,
        };
        #[derive(Debug)]
        AnotherCollection {
            some_field: String
        };
    }
}

// all constants that were defined
assert_eq!("myDatabase", mongo::MyDatabase::NAME);
assert_eq!("myFirstCollection", mongo::schema::MyFirstCollection::NAME);
assert_eq!(1, mongo::schema::MyFirstCollection::SCHEMA_VERSION);
assert_eq!("anotherCollection", mongo::schema::AnotherCollection::NAME);
assert_eq!(1, mongo::schema::AnotherCollection::SCHEMA_VERSION);

// initializer function and general usage
// note that `tokio_test::block_on` is just a test function to run `async` code in doc tests

let mongo = tokio_test::block_on(mongo::MyDatabase::new("mongodb://example.com"))
    .expect("Could not create mongoDB client");

let bob = mongo::schema::MyFirstCollection {
    _id: None,
    first_name: String::from("Bob"),
    last_name: String::from("Bob's last name"),
    age: 255,
};

// This should fail beause there is no actual mongoDB service running at the specified
// connection.
assert!(tokio_test::block_on(
    mongo.my_first_collection_coll.insert_one(bob, None)
).is_err());

§Manipulating / Removing _id

You can specify any type (that implements Serialize and Deserialize) to be used inside the _id Option by specifying it in < / > after the collection name:

use mongodb_ext::mongo_db;

mongo_db! {
    SomeDatabase {
        SomeCollection<_id: u128> {
            first_name: String,
        }
    }
}

// _id is now `u128` instead of `DefaultId`
let some_document = mongo::schema::SomeCollection {
    _id: Some(255),
    first_name: String::from("Bob")
};

It is also possible to disable the generation of an _id field all together by using <_id: none>.

use mongodb_ext::mongo_db;

mongo_db! {
    SomeDatabase {
        SomeCollection<_id: none> {
            #[serde(skip_serializing_if = "Option::is_none")]
            email_address: Option<String>,
            first_name: String,
        }
    }
}

// no `_id` exists, this example assumes that users are addressed via their email address
let some_document = mongo::schema::SomeCollection {
    email_address: Some(String::from("bob@example.com")),
    first_name: String::from("Bob")
};

These features are unique for each collection:

use mongodb_ext::{mongo_db, DefaultId};

mongo_db! {
    SomeDatabase {
        SomeCollection<_id: u128> {
            first_name: String,
        };
        Another {
            some_field: u32,
        };
        AndYetAnother<_id: none> {
            email: String,
            name: String,
        }
    }
}

// `_id` type changed to `u128`
let some_document = mongo::schema::SomeCollection {
    _id: Some(255),
    first_name: String::from("Bob")
};
// `_id` type default, eg. `DefaultId`
let oid = DefaultId::parse_str("0123456789ABCDEF01234567").unwrap();
let another_document = mongo::schema::Another {
    _id: Some(oid),
    some_field: 1,
};
// `_id` field disabled
let and_yet_another_document = mongo::schema::AndYetAnother {
    name: String::from("Bob"),
    email: String::from("bob@example.com")
};

Each collection that does not have a parameter of id: none implements a function id(&self) that returns a reference to its ID:

use mongodb_ext::{mongo_db, DefaultId};

mongo_db! {
    SomeDatabase {
        SomeCollection<_id: u128> {};
        Another {};
    }
}

// `id` returns `&Option<u128>`
let some_collection = mongo::schema::SomeCollection {
    _id: Some(255),
};
assert_eq!(
    *some_collection.id(),
    Some(255)
);

// `id` returns `&Option<DefaultId>`
let oid = DefaultId::parse_str("0123456789ABCDEF01234567").unwrap();
let another = mongo::schema::Another {
    _id: Some(oid.clone()),
};
assert_eq!(
    *another.id(),
    Some(oid)
);

§Versioning of your schema

Your database schema version is managed via MongoCollection::SCHEMA_VERSION.

This can be modified like so:

use mongodb_ext::{mongo_db, MongoCollection};
use serde_json::ser;

mongo_db! {
    SomeDatabase {
        // no schema version defaults to const `DEFAULT_SCHEMA_VERSION`
        Items {
            name: String,
        };
        // schema version of 200
        Queue<version: 200> {
            item: i32,
        };
        // schema version of 4
        SomeCollection<version: 4, _id: none> {
            first_name: String,
        };
        // schema version of 5
        FourthCollection<_id: String, version: 5> {};
    }
}

// default schema version is 1
assert_eq!(1, mongodb_ext::DEFAULT_SCHEMA_VERSION);

assert_eq!(mongo::schema::Items::SCHEMA_VERSION, 1);
assert_eq!(mongo::schema::Queue::SCHEMA_VERSION, 200);
assert_eq!(mongo::schema::SomeCollection::SCHEMA_VERSION, 4);
assert_eq!(mongo::schema::FourthCollection::SCHEMA_VERSION, 5);

§Serializing from json! and doc! macros

use mongodb_ext::mongo_db;
use serde_json::{json, Value};
use mongodb::{bson::{doc, Document}, bson};

mongo_db! {
    #[derive(Debug, Clone)]
    DatabaseOfItems {
        #[derive(Debug, Clone, PartialEq)]
        Items {
            counter: u16,
            name: String
        };
    }
}

// Note that `_id` is not specified here
let my_item: Value = json! ({
    "counter": 0,
    "name": "my_special_item"
});

let my_collection_entry: mongo::schema::Items =
    serde_json::from_value(my_item)
    .expect("Could not convert json Value to collection document");

assert_eq!(
    my_collection_entry,
    mongo::schema::Items {
        _id: None,
        counter: 0,
        name: String::from("my_special_item")
    }
);

// Note that `_id` is not specified here
let my_item: Document = doc! {
    "counter": 0,
    "name": "my_special_item"
};

let my_collection_entry: mongo::schema::Items = bson::de::from_document(my_item)
    .expect("Could not convert mongodb bson Document to collection document");

assert_eq!(
    my_collection_entry,
    mongo::schema::Items {
        _id: None,
        counter: 0,
        name: String::from("my_special_item")
    }
);

§Adding your own code

Additional code for the mongo and schema modules can be specified in curly braces ({ / }).

use mongodb_ext::mongo_db;

mongo_db! {
    // specify code to be in `mongo` here:
    {
        pub fn this_is_a_function_in_mongo() -> bool { true }
    }
    SomeDatabase {
        // specify code to be in `schema` here:
        {
            pub fn this_is_a_function_in_schema() -> bool { true }
            use std::collections::HashMap;
        }
        SomeCollection {
            dict: HashMap<String, u32>,
        }
    }
}

assert!(mongo::this_is_a_function_in_mongo());
assert!(mongo::schema::this_is_a_function_in_schema());

§Code positioning

Implementations can be easily added by using the preset feature:

use mongodb_ext::{mongo_db, DefaultId};

mongo_db! {
    // specify globally needed code in `mongo` here:
    {
        use std::collections::HashMap;
    }
    SomeDatabase {
        // specify globally needed code in `schema` here:
        {
            use {
                std::collections::HashMap,
                mongodb::bson::oid::ObjectId
            };
        }

        // specify collection-dependent code in an additional block below the
        // collection connected with a `-`:
        SomeCollection {
            dict: HashMap<String, u32>,
        }-{
            pub fn some_collection_function() -> bool { true }
        };
        #[derive(Debug, PartialEq)]
        AnotherCollection {}-{
            pub fn from_id(id: ObjectId) -> Self { Self { _id: Some(id) } }
        }
    }-{
        // specify implementations on the database handler here:
        pub fn give_bool() -> bool { true }
    }
}

assert!(mongo::SomeDatabase::give_bool());
assert!(mongo::schema::SomeCollection::some_collection_function());

let oid = DefaultId::parse_str("0123456789ABCDEF01234567").unwrap();
assert_eq!(
    mongo::schema::AnotherCollection::from_id(oid.clone()),
    mongo::schema::AnotherCollection {
        _id: Some(oid),
    },
);

§TypedBuilder

Each schema implements TypedBuilder which lets you create a collection more easily.

If _id is not set to none, the _id field will have a builder attribute set to default. This enables you to skip specifying _id as None.

use mongodb_ext::{mongo_db, MongoClient, MongoCollection};

mongo_db! {
    MyDatabase {
        #[derive(Debug, PartialEq)]
        MyCollection<version: 2, _id: u128> {
            name: String,
            counter: u32,
            schema_version: i32
        }
    }
}

use mongo::schema::MyCollection;

assert_eq!(
    // constructing using the builder
    // note that no field `_id` is specified, thus `None` is used
    MyCollection::builder()
        .name("Alice".to_string())
        .counter(1)
        .schema_version(MyCollection::SCHEMA_VERSION)
        .build(),
    // constructing normally
    MyCollection {
        _id: None,
        name: "Alice".to_string(),
        counter: 1,
        schema_version: MyCollection::SCHEMA_VERSION
    }
);

Combining the schema version with the typed builder can be very useful:

use mongodb_ext::{mongo_db, MongoClient, MongoCollection};

mongo_db! {
    MyDatabase {
        {
            use mongodb_ext::MongoCollection;
        }
        #[derive(Debug, PartialEq)]
        MyCollection<version: 2, _id: u128> {
            name: String,
            counter: u32,
            #[builder(default = <MyCollection as MongoCollection>::SCHEMA_VERSION)]
            schema_version: i32
        }
    }
}

use mongo::schema::MyCollection;

assert_eq!(
    // specifying no version takes version constant by default
    MyCollection::builder()
        .name("Alice".to_string())
        .counter(255)
        .build(),
    MyCollection {
        _id: None,
        name: "Alice".to_string(),
        counter: 255,
        schema_version: 2
    }
);