use bson::{Bson, Document, oid::ObjectId};
use prax_query::filter::FilterValue;
use crate::error::{MongoError, MongoResult};
pub fn filter_value_to_bson(value: &FilterValue) -> MongoResult<Bson> {
match value {
FilterValue::Null => Ok(Bson::Null),
FilterValue::Bool(b) => Ok(Bson::Boolean(*b)),
FilterValue::Int(i) => Ok(Bson::Int64(*i)),
FilterValue::Float(f) => Ok(Bson::Double(*f)),
FilterValue::String(s) => {
if s.len() == 24 && s.chars().all(|c| c.is_ascii_hexdigit()) {
if let Ok(oid) = ObjectId::parse_str(s) {
return Ok(Bson::ObjectId(oid));
}
}
Ok(Bson::String(s.clone()))
}
FilterValue::Json(j) => {
let bson = bson::to_bson(j).map_err(|e| {
MongoError::serialization(format!("failed to convert JSON to BSON: {}", e))
})?;
Ok(bson)
}
FilterValue::List(list) => {
let bson_values: Result<Vec<Bson>, _> = list.iter().map(filter_value_to_bson).collect();
Ok(Bson::Array(bson_values?))
}
}
}
pub fn filter_values_to_bson(values: &[FilterValue]) -> MongoResult<Vec<Bson>> {
values.iter().map(filter_value_to_bson).collect()
}
pub mod mongo_types {
pub fn rust_type_to_bson(rust_type: &str) -> Option<&'static str> {
match rust_type {
"i8" | "i16" | "i32" => Some("int"),
"i64" => Some("long"),
"f32" | "f64" => Some("double"),
"bool" => Some("bool"),
"String" | "&str" => Some("string"),
"Vec<u8>" | "&[u8]" => Some("binData"),
"chrono::NaiveDate" | "chrono::NaiveDateTime" | "chrono::DateTime<chrono::Utc>" => {
Some("date")
}
"uuid::Uuid" => Some("binData"), "serde_json::Value" => Some("object"),
"bson::oid::ObjectId" => Some("objectId"),
"rust_decimal::Decimal" => Some("decimal"),
_ => None,
}
}
pub fn bson_type_to_rust(bson_type: &str) -> &'static str {
match bson_type {
"double" => "f64",
"string" => "String",
"object" => "Document",
"array" => "Vec<Bson>",
"binData" => "Vec<u8>",
"objectId" => "bson::oid::ObjectId",
"bool" => "bool",
"date" => "chrono::DateTime<chrono::Utc>",
"null" => "Option<()>",
"int" => "i32",
"long" => "i64",
"decimal" => "rust_decimal::Decimal",
"timestamp" => "bson::Timestamp",
"regex" => "bson::Regex",
_ => "Bson",
}
}
pub fn bson_type_to_prax(bson_type: &str) -> &'static str {
match bson_type {
"double" => "Float",
"string" => "String",
"object" => "Json",
"array" => "List",
"binData" => "Bytes",
"objectId" => "String", "bool" => "Boolean",
"date" => "DateTime",
"null" => "Null",
"int" => "Int",
"long" => "BigInt",
"decimal" => "Decimal",
_ => "Unknown",
}
}
pub fn prax_type_to_bson(prax_type: &str) -> &'static str {
match prax_type {
"Boolean" => "bool",
"Int" => "int",
"BigInt" => "long",
"Float" => "double",
"Decimal" => "decimal",
"String" => "string",
"Bytes" => "binData",
"Date" => "date",
"Time" => "string", "DateTime" => "date",
"Uuid" => "binData", "Json" => "object",
_ => "string",
}
}
}
pub mod aggregation {
use super::*;
pub fn match_stage(filter: Document) -> Document {
bson::doc! { "$match": filter }
}
pub fn project_stage(projection: Document) -> Document {
bson::doc! { "$project": projection }
}
pub fn group_stage(id: Bson, accumulators: Document) -> Document {
let mut stage = bson::doc! { "_id": id };
stage.extend(accumulators);
bson::doc! { "$group": stage }
}
pub fn sort_stage(sort: Document) -> Document {
bson::doc! { "$sort": sort }
}
pub fn limit_stage(limit: i64) -> Document {
bson::doc! { "$limit": limit }
}
pub fn skip_stage(skip: i64) -> Document {
bson::doc! { "$skip": skip }
}
pub fn lookup_stage(
from: &str,
local_field: &str,
foreign_field: &str,
as_field: &str,
) -> Document {
bson::doc! {
"$lookup": {
"from": from,
"localField": local_field,
"foreignField": foreign_field,
"as": as_field
}
}
}
pub fn unwind_stage(path: &str, preserve_null_and_empty: bool) -> Document {
bson::doc! {
"$unwind": {
"path": format!("${}", path),
"preserveNullAndEmptyArrays": preserve_null_and_empty
}
}
}
pub fn count_stage(field_name: &str) -> Document {
bson::doc! { "$count": field_name }
}
pub fn add_fields_stage(fields: Document) -> Document {
bson::doc! { "$addFields": fields }
}
pub fn set_stage(fields: Document) -> Document {
bson::doc! { "$set": fields }
}
pub fn unset_stage(fields: Vec<&str>) -> Document {
bson::doc! { "$unset": fields }
}
pub fn replace_root_stage(new_root: Bson) -> Document {
bson::doc! { "$replaceRoot": { "newRoot": new_root } }
}
pub fn sum(expression: impl Into<Bson>) -> Document {
bson::doc! { "$sum": expression.into() }
}
pub fn avg(expression: impl Into<Bson>) -> Document {
bson::doc! { "$avg": expression.into() }
}
pub fn min(expression: impl Into<Bson>) -> Document {
bson::doc! { "$min": expression.into() }
}
pub fn max(expression: impl Into<Bson>) -> Document {
bson::doc! { "$max": expression.into() }
}
pub fn first(expression: impl Into<Bson>) -> Document {
bson::doc! { "$first": expression.into() }
}
pub fn last(expression: impl Into<Bson>) -> Document {
bson::doc! { "$last": expression.into() }
}
pub fn push(expression: impl Into<Bson>) -> Document {
bson::doc! { "$push": expression.into() }
}
pub fn add_to_set(expression: impl Into<Bson>) -> Document {
bson::doc! { "$addToSet": expression.into() }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filter_value_to_bson_primitives() {
assert_eq!(
filter_value_to_bson(&FilterValue::Int(42)).unwrap(),
Bson::Int64(42)
);
assert_eq!(
filter_value_to_bson(&FilterValue::Float(3.14)).unwrap(),
Bson::Double(3.14)
);
assert_eq!(
filter_value_to_bson(&FilterValue::Bool(true)).unwrap(),
Bson::Boolean(true)
);
assert_eq!(
filter_value_to_bson(&FilterValue::Null).unwrap(),
Bson::Null
);
}
#[test]
fn test_filter_value_to_bson_string() {
let result = filter_value_to_bson(&FilterValue::String("hello".to_string())).unwrap();
assert_eq!(result, Bson::String("hello".to_string()));
}
#[test]
fn test_filter_value_to_bson_object_id() {
let oid = ObjectId::new();
let result = filter_value_to_bson(&FilterValue::String(oid.to_hex())).unwrap();
assert_eq!(result, Bson::ObjectId(oid));
}
#[test]
fn test_filter_value_to_bson_list() {
let list = vec![
FilterValue::Int(1),
FilterValue::Int(2),
FilterValue::Int(3),
];
let result = filter_value_to_bson(&FilterValue::List(list)).unwrap();
assert_eq!(
result,
Bson::Array(vec![Bson::Int64(1), Bson::Int64(2), Bson::Int64(3)])
);
}
#[test]
fn test_type_mappings() {
use mongo_types::*;
assert_eq!(rust_type_to_bson("i32"), Some("int"));
assert_eq!(rust_type_to_bson("String"), Some("string"));
assert_eq!(rust_type_to_bson("bool"), Some("bool"));
assert_eq!(bson_type_to_rust("int"), "i32");
assert_eq!(bson_type_to_rust("string"), "String");
assert_eq!(bson_type_to_rust("bool"), "bool");
assert_eq!(prax_type_to_bson("Int"), "int");
assert_eq!(prax_type_to_bson("String"), "string");
assert_eq!(prax_type_to_bson("Boolean"), "bool");
}
#[test]
fn test_aggregation_stages() {
use aggregation::*;
let match_doc = match_stage(bson::doc! { "status": "active" });
assert!(match_doc.contains_key("$match"));
let sort_doc = sort_stage(bson::doc! { "created_at": -1 });
assert!(sort_doc.contains_key("$sort"));
let limit_doc = limit_stage(10);
assert!(limit_doc.contains_key("$limit"));
}
}