use indexmap::IndexMap;
use parse_rust_core::{ParseMap, ParseValue};
pub type PointersByClass = IndexMap<String, Vec<String>>;
pub fn collect_pointers(results: &[ParseMap], path: &[String]) -> PointersByClass {
let mut out: PointersByClass = IndexMap::new();
for row in results {
collect_from_value(&ParseValue::Object(row.clone()), path, &mut out);
}
out
}
fn collect_from_value(value: &ParseValue, path: &[String], out: &mut PointersByClass) {
if let ParseValue::Array(items) = value {
for item in items {
collect_from_value(item, path, out);
}
return;
}
match (path.split_first(), value) {
(
None,
ParseValue::Pointer {
class_name,
object_id,
},
) => {
let ids = out.entry(class_name.clone()).or_default();
if !ids.contains(object_id) {
ids.push(object_id.clone());
}
}
(None, _) => {}
(Some((head, rest)), ParseValue::Object(map)) => {
if let Some(next) = map.get(head) {
collect_from_value(next, rest, out);
}
}
_ => {}
}
}
pub fn graft(results: &mut [ParseMap], path: &[String], fetched: &IndexMap<String, ParseMap>) {
for row in results.iter_mut() {
graft_into_map(row, path, fetched);
}
}
fn graft_into_map(map: &mut ParseMap, path: &[String], fetched: &IndexMap<String, ParseMap>) {
let Some((head, rest)) = path.split_first() else {
return;
};
let Some(current) = map.shift_remove(head) else {
return;
};
if let Some(value) = graft_value(current, rest, fetched) {
map.insert(head.clone(), value);
}
}
fn graft_value(
value: ParseValue,
path: &[String],
fetched: &IndexMap<String, ParseMap>,
) -> Option<ParseValue> {
if let ParseValue::Array(items) = value {
return Some(ParseValue::Array(
items
.into_iter()
.filter_map(|item| graft_value(item, path, fetched))
.collect(),
));
}
match (path.split_first(), value) {
(None, ParseValue::Pointer { object_id, .. }) => fetched
.get(&object_id)
.map(|row| ParseValue::Object(row.clone())),
(None, other) => Some(other),
(Some((head, rest)), ParseValue::Object(mut map)) => {
if let Some(inner) = map.shift_remove(head) {
if let Some(replaced) = graft_value(inner, rest, fetched) {
map.insert(head.clone(), replaced);
}
}
Some(ParseValue::Object(map))
}
(Some(_), other) => Some(other),
}
}
pub fn shape_included(row: &mut ParseMap, class_name: &str, is_master: bool) {
row.insert(
"__type".to_string(),
ParseValue::String("Object".to_string()),
);
row.insert(
"className".to_string(),
ParseValue::String(class_name.to_string()),
);
if class_name == "_User" && !is_master {
row.shift_remove("sessionToken");
row.shift_remove("authData");
}
}
pub fn keys_for_path(keys: &[String], path: &[String]) -> Option<Vec<String>> {
let mut out: Vec<String> = Vec::new();
for key in keys {
let parts: Vec<&str> = key.split('.').collect();
if !path
.iter()
.enumerate()
.all(|(i, p)| parts.get(i).is_some_and(|k| k == p))
{
continue;
}
if let Some(next) = parts.get(path.len()) {
let next = (*next).to_string();
if !out.contains(&next) {
out.push(next);
}
}
}
(!out.is_empty()).then_some(out)
}
pub fn exclude_keys_for_path(exclude_keys: &[String], path: &[String]) -> Option<Vec<String>> {
let mut out: Vec<String> = Vec::new();
for key in exclude_keys {
let parts: Vec<&str> = key.split('.').collect();
if !path
.iter()
.enumerate()
.all(|(i, p)| parts.get(i).is_some_and(|k| k == p))
{
continue;
}
if path.len() == parts.len().saturating_sub(1) {
if let Some(next) = parts.get(path.len()) {
let next = (*next).to_string();
if !out.contains(&next) {
out.push(next);
}
}
}
}
(!out.is_empty()).then_some(out)
}
pub fn paths_forced_by_projection(keys: &[String], exclude_keys: &[String]) -> Vec<String> {
keys.iter()
.chain(exclude_keys.iter())
.filter(|k| k.contains('.'))
.filter_map(|k| k.rsplit_once('.').map(|(head, _)| head.to_string()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn pointer(class: &str, id: &str) -> ParseValue {
ParseValue::Pointer {
class_name: class.to_string(),
object_id: id.to_string(),
}
}
fn row(pairs: Vec<(&str, ParseValue)>) -> ParseMap {
let mut m = ParseMap::new();
for (k, v) in pairs {
m.insert(k.to_string(), v);
}
m
}
#[test]
fn pointers_group_by_class_and_dedupe() {
let results = vec![
row(vec![("author", pointer("_User", "u1"))]),
row(vec![("author", pointer("_User", "u1"))]),
row(vec![("author", pointer("Robot", "r1"))]),
];
let found = collect_pointers(&results, &["author".to_string()]);
assert_eq!(found.get("_User"), Some(&vec!["u1".to_string()]));
assert_eq!(found.get("Robot"), Some(&vec!["r1".to_string()]));
}
#[test]
fn pointers_inside_arrays_are_found() {
let results = vec![row(vec![(
"editors",
ParseValue::Array(vec![pointer("_User", "u1"), pointer("_User", "u2")]),
)])];
let found = collect_pointers(&results, &["editors".to_string()]);
assert_eq!(
found.get("_User"),
Some(&vec!["u1".to_string(), "u2".to_string()])
);
}
#[test]
fn a_nested_path_reaches_through_an_expanded_parent() {
let results = vec![row(vec![(
"author",
ParseValue::Object(row(vec![("company", pointer("Company", "c1"))])),
)])];
let found = collect_pointers(&results, &["author".to_string(), "company".to_string()]);
assert_eq!(found.get("Company"), Some(&vec!["c1".to_string()]));
}
#[test]
fn a_resolved_pointer_is_replaced_and_an_unresolved_one_disappears() {
let mut results = vec![
row(vec![
("objectId", ParseValue::String("p1".into())),
("author", pointer("_User", "u1")),
]),
row(vec![
("objectId", ParseValue::String("p2".into())),
("author", pointer("_User", "hidden")),
]),
];
let mut fetched = IndexMap::new();
fetched.insert(
"u1".to_string(),
row(vec![("objectId", ParseValue::String("u1".into()))]),
);
graft(&mut results, &["author".to_string()], &fetched);
assert!(matches!(
results[0].get("author"),
Some(ParseValue::Object(_))
));
assert!(
results[1].get("author").is_none(),
"a pointer the caller cannot read is dropped, not left as a pointer"
);
}
#[test]
fn an_unresolved_pointer_inside_an_array_is_filtered_out() {
let mut results = vec![row(vec![(
"editors",
ParseValue::Array(vec![pointer("_User", "u1"), pointer("_User", "hidden")]),
)])];
let mut fetched = IndexMap::new();
fetched.insert(
"u1".to_string(),
row(vec![("objectId", ParseValue::String("u1".into()))]),
);
graft(&mut results, &["editors".to_string()], &fetched);
match results[0].get("editors") {
Some(ParseValue::Array(items)) => assert_eq!(items.len(), 1),
other => panic!("expected an array, got {other:?}"),
}
}
#[test]
fn an_included_user_loses_its_session_token_for_a_non_master_caller() {
let mut r = row(vec![
("sessionToken", ParseValue::String("r:t".into())),
("authData", ParseValue::Object(ParseMap::new())),
]);
shape_included(&mut r, "_User", false);
assert!(r.get("sessionToken").is_none());
assert!(r.get("authData").is_none());
assert!(matches!(r.get("__type"), Some(ParseValue::String(s)) if s == "Object"));
assert!(matches!(r.get("className"), Some(ParseValue::String(s)) if s == "_User"));
let mut r = row(vec![("sessionToken", ParseValue::String("r:t".into()))]);
shape_included(&mut r, "_User", true);
assert!(r.get("sessionToken").is_some());
}
#[test]
fn projections_rewrite_per_path() {
let keys = vec!["author.name".to_string(), "title".to_string()];
assert_eq!(
keys_for_path(&keys, &["author".to_string()]),
Some(vec!["name".to_string()])
);
assert_eq!(keys_for_path(&keys, &["other".to_string()]), None);
let excludes = vec!["author.company.name".to_string()];
assert_eq!(
exclude_keys_for_path(&excludes, &["author".to_string()]),
None
);
assert_eq!(
exclude_keys_for_path(&excludes, &["author".to_string(), "company".to_string()]),
Some(vec!["name".to_string()])
);
}
#[test]
fn a_dotted_projection_forces_its_parent_include() {
assert_eq!(
paths_forced_by_projection(&["a.b.c".to_string(), "d".to_string()], &[]),
vec!["a.b".to_string()]
);
assert_eq!(
paths_forced_by_projection(&[], &["x.y".to_string()]),
vec!["x".to_string()]
);
}
}