use crate::storage::Error;
use mongodb::bson::{DateTime, oid::ObjectId};
use pbbson::Model;
use pbbson::bson::spec::BinarySubtype;
use pbbson::bson::{self, Bson, binary};
use std::str::FromStr;
use std::time::SystemTime;
pub(crate) fn now_as_datetime() -> DateTime {
let now = SystemTime::now();
DateTime::from_system_time(now)
}
pub(crate) fn try_to_object_id(id: &str) -> Result<ObjectId, Error> {
const WHAT: &str = "IDs";
require_xid_base32hex(WHAT, id)?;
let as_xid = match xid::Id::from_str(id) {
Ok(xid) => xid,
Err(e) => {
let msg = format!("{WHAT} must be 12-byte XID's in 20-byte base32hex encoded form ({e:?})");
return Err(Error::invalid_argument(msg));
}
};
Ok(ObjectId::from_bytes(*as_xid.as_bytes()))
}
pub(crate) fn try_to_uuid(id: &str) -> Result<bson::Binary, Error> {
let uuid = bson::Uuid::parse_str(id).map_err(|e| Error::internal(e.to_string()))?;
Ok(binary::Binary::from_uuid(uuid))
}
pub(crate) fn require_xid_base32hex(what: &str, id: &str) -> Result<(), Error> {
if id.len() != 20 {
return Err(Error::invalid_argument(format!(
"{what} must be 12-byte XID's in 20-byte base32hex encoded form"
)));
}
Ok(())
}
pub(crate) fn transform_to_mongo(model: Model) -> Result<Model, Error> {
let mut local_model = Model::default();
let local_model_id = match model.get("id") {
Some(Bson::ObjectId(object_id)) => Some(*object_id),
Some(Bson::String(id)) => Some(try_to_object_id(id)?),
_ => None,
};
if let Some(local_model_id) = local_model_id {
local_model.insert("_id", local_model_id);
}
for (k, v) in model.iter().filter(|(k, _v)| *k != "id") {
if k.ends_with("Id") {
match v {
Bson::ObjectId(object_id) => {
local_model.insert(k, *object_id);
}
Bson::String(id) => {
if id.is_empty() {
local_model.insert(k, Bson::Null);
} else if let Ok(object_id) = try_to_object_id(id) {
local_model.insert(k, object_id);
} else if let Ok(uuid) = try_to_uuid(id) {
local_model.insert(k, Bson::Binary(uuid));
} else {
local_model.insert(k, v);
}
}
_ => {
local_model.insert(k, v);
}
};
} else {
local_model.insert(k, v);
}
}
Ok(local_model)
}
pub(crate) fn transform_from_mongo(model: Model) -> Result<Model, Error> {
let mut model = model.clone();
for (k, v) in model.clone().iter() {
match v {
Bson::Null => {
model.remove(k);
}
Bson::Binary(binary) if binary.subtype == BinarySubtype::Uuid && k == "_id" => {
let uuid = binary.to_uuid().map_err(|e| Error::internal(e.to_string()))?;
model.remove("_id");
model.insert("id", uuid.to_string());
}
Bson::ObjectId(object_id) => {
if let Ok(id) = xid::Id::from_bytes(&object_id.bytes()) {
if k == "_id" {
model.remove("_id");
model.insert("id", id.to_string());
} else {
model.insert(k, id.to_string());
}
}
}
_ => {}
}
}
Ok(model)
}
#[cfg(test)]
mod tests {
use crate::storage::config_store::adapters::mongodb::serde::transform_from_mongo;
use pbbson::Model;
use pbbson::bson::oid::ObjectId;
use pbbson::bson::{Bson, doc};
#[test]
fn can_transform_object_ids_to_protobuf() {
let input = Model::from(doc! {
"_id": ObjectId::parse_str("68571bb59bcb60707c7c3aac").unwrap(),
"serviceId": ObjectId::parse_str("68571bb59bcb60707c7c3aad").unwrap(),
"planId": ObjectId::parse_str("68571bb59bcb60707c7c3aae").unwrap(),
});
let output = transform_from_mongo(input).unwrap();
assert_eq!(
output.get("id"),
Some(&Bson::String("d1bhndcrpdg70v3s7am0".to_string()))
);
assert_eq!(
output.get("serviceId"),
Some(&Bson::String("d1bhndcrpdg70v3s7amg".to_string()))
);
assert_eq!(
output.get("planId"),
Some(&Bson::String("d1bhndcrpdg70v3s7an0".to_string()))
);
}
#[test]
fn can_transform_null_string_to_required_defaultable_protobuf_field() {
let input = Model::from(doc! {
"providerUid": Bson::Null,
});
let output = transform_from_mongo(input).unwrap();
assert_eq!(output.get("providerUid"), None);
}
}