use std::io::Write;
use std::process::{Command, Stdio};
use parse_rust_core::{ParseDate, ParseMap, ParseValue};
use parse_rust_mongo::transform::parse_object_to_mongo_create;
use parse_rust_storage::{ClassSchema, FieldType};
struct Case {
name: &'static str,
schema: ClassSchema,
object: ParseMap,
object_json: serde_json::Value,
}
fn m(pairs: Vec<(&str, ParseValue)>) -> ParseMap {
let mut map = ParseMap::new();
for (k, v) in pairs {
map.insert(k.to_string(), v);
}
map
}
fn schema_json(schema: &ClassSchema) -> serde_json::Value {
let mut fields = serde_json::Map::new();
for (name, ty) in &schema.fields {
let mut entry = serde_json::Map::new();
match ty {
FieldType::Pointer { target_class } => {
entry.insert("type".into(), "Pointer".into());
entry.insert("targetClass".into(), target_class.clone().into());
}
other => {
entry.insert("type".into(), other.to_wire_string().into());
}
}
fields.insert(name.clone(), serde_json::Value::Object(entry));
}
serde_json::json!({ "fields": fields })
}
fn cases() -> Vec<Case> {
let post = ClassSchema::new("Post")
.with_field("title", FieldType::String)
.with_field("views", FieldType::Number)
.with_field("live", FieldType::Boolean)
.with_field("when", FieldType::Date)
.with_field("tags", FieldType::Array)
.with_field("meta", FieldType::Object)
.with_field(
"author",
FieldType::Pointer {
target_class: "_User".into(),
},
);
let iso = "2026-08-14T13:34:33.581Z";
let date = ParseDate::parse_iso(iso).expect("test date");
vec![
Case {
name: "strings and booleans",
schema: post.clone(),
object: m(vec![
("title", ParseValue::String("hello".into())),
("live", ParseValue::Bool(true)),
]),
object_json: serde_json::json!({ "title": "hello", "live": true }),
},
Case {
name: "integral number stores as Int32",
schema: post.clone(),
object: m(vec![("views", ParseValue::Number(7.0))]),
object_json: serde_json::json!({ "views": 7 }),
},
Case {
name: "zero stores as Int32",
schema: post.clone(),
object: m(vec![("views", ParseValue::Number(0.0))]),
object_json: serde_json::json!({ "views": 0 }),
},
Case {
name: "negative integral stores as Int32",
schema: post.clone(),
object: m(vec![("views", ParseValue::Number(-42.0))]),
object_json: serde_json::json!({ "views": -42 }),
},
Case {
name: "i32 boundary stores as Int32",
schema: post.clone(),
object: m(vec![("views", ParseValue::Number(i32::MAX as f64))]),
object_json: serde_json::json!({ "views": i32::MAX }),
},
Case {
name: "just past i32 stores as Double",
schema: post.clone(),
object: m(vec![("views", ParseValue::Number(i32::MAX as f64 + 1.0))]),
object_json: serde_json::json!({ "views": i32::MAX as f64 + 1.0 }),
},
Case {
name: "fractional stores as Double",
schema: post.clone(),
object: m(vec![("views", ParseValue::Number(1.5))]),
object_json: serde_json::json!({ "views": 1.5 }),
},
Case {
name: "date becomes a BSON date",
schema: post.clone(),
object: m(vec![("when", ParseValue::Date(date))]),
object_json: serde_json::json!({ "when": { "__type": "Date", "iso": iso } }),
},
Case {
name: "declared pointer collapses to Class$id",
schema: post.clone(),
object: m(vec![(
"author",
ParseValue::Pointer {
class_name: "_User".into(),
object_id: "abc123".into(),
},
)]),
object_json: serde_json::json!({
"author": { "__type": "Pointer", "className": "_User", "objectId": "abc123" }
}),
},
Case {
name: "array of scalars",
schema: post.clone(),
object: m(vec![(
"tags",
ParseValue::Array(vec![
ParseValue::String("a".into()),
ParseValue::Number(2.0),
ParseValue::Bool(false),
]),
)]),
object_json: serde_json::json!({ "tags": ["a", 2, false] }),
},
Case {
name: "nested object keeps its shape",
schema: post.clone(),
object: m(vec![(
"meta",
ParseValue::Object(m(vec![
("k", ParseValue::String("v".into())),
("n", ParseValue::Number(3.0)),
])),
)]),
object_json: serde_json::json!({ "meta": { "k": "v", "n": 3 } }),
},
Case {
name: "nested date inside an array",
schema: post.clone(),
object: m(vec![(
"tags",
ParseValue::Array(vec![ParseValue::Date(date)]),
)]),
object_json: serde_json::json!({
"tags": [{ "__type": "Date", "iso": iso }]
}),
},
Case {
name: "nested pointer keeps its envelope",
schema: post.clone(),
object: m(vec![(
"tags",
ParseValue::Array(vec![ParseValue::Pointer {
class_name: "Tag".into(),
object_id: "t1".into(),
}]),
)]),
object_json: serde_json::json!({
"tags": [{ "__type": "Pointer", "className": "Tag", "objectId": "t1" }]
}),
},
Case {
name: "null",
schema: post.clone(),
object: m(vec![("title", ParseValue::Null)]),
object_json: serde_json::json!({ "title": null }),
},
Case {
name: "objectId is renamed to _id",
schema: post.clone(),
object: m(vec![("objectId", ParseValue::String("oid1".into()))]),
object_json: serde_json::json!({ "objectId": "oid1" }),
},
]
}
#[test]
#[ignore = "requires node and the upstream checkout; run via tools/test.sh"]
fn our_transform_matches_upstreams() {
let payload: Vec<serde_json::Value> = cases()
.into_iter()
.map(|c| {
let doc = parse_object_to_mongo_create(&c.schema, &c.object)
.unwrap_or_else(|e| panic!("{}: our transform failed: {e}", c.name));
let ejson = bson::Bson::Document(doc)
.into_canonical_extjson()
.to_string();
serde_json::json!({
"name": c.name,
"className": c.schema.class_name,
"schema": schema_json(&c.schema),
"object": c.object_json,
"rustBson": ejson,
})
})
.collect();
let oracle = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tools/mongo-transform-oracle.js"
);
let mut child = Command::new("node")
.env_remove("NODE_OPTIONS")
.arg(oracle)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("node must be on PATH; this test is #[ignore]d by default");
child
.stdin
.take()
.expect("stdin piped")
.write_all(
serde_json::to_string(&payload)
.expect("serialize")
.as_bytes(),
)
.expect("write to node");
let out = child.wait_with_output().expect("node ran");
assert!(
out.status.success(),
"transform diverges from MongoTransform.\n{}\n{}",
String::from_utf8_lossy(&out.stderr),
String::from_utf8_lossy(&out.stdout),
);
println!("{}", String::from_utf8_lossy(&out.stdout).trim());
}