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 structmongo::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 insnake_case
. - It also contains a
client
and adatabase
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
Impl
ementations 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
}
);