use serde_json::{Map, Value, json};
use std::collections::HashSet;
#[allow(clippy::implicit_hasher)] #[must_use]
pub fn project_json(value: &Value, selected_fields: &HashSet<String>) -> Value {
match value {
Value::Object(map) => {
let mut projected = Map::new();
for (key, val) in map {
let key_lower = key.to_lowercase();
if selected_fields.contains(&key_lower) {
projected.insert(key.clone(), val.clone());
} else {
let nested_fields = extract_nested_fields(&key_lower, selected_fields);
if !nested_fields.is_empty() {
projected.insert(key.clone(), project_json(val, &nested_fields));
}
}
}
Value::Object(projected)
}
Value::Array(arr) => Value::Array(
arr.iter()
.map(|v| project_json(v, selected_fields))
.collect(),
),
other => other.clone(),
}
}
fn extract_nested_fields(parent_key: &str, selected_fields: &HashSet<String>) -> HashSet<String> {
let prefix = format!("{parent_key}.");
selected_fields
.iter()
.filter(|field| field.starts_with(&prefix))
.map(|field| field[prefix.len()..].to_string())
.collect()
}
pub fn apply_select<T: serde::Serialize>(value: T, selected_fields: Option<&[String]>) -> Value {
match selected_fields {
Some(fields) if !fields.is_empty() => {
let fields_set: HashSet<String> = fields.iter().map(|f| f.to_lowercase()).collect();
match serde_json::to_value(value) {
Ok(v) => project_json(&v, &fields_set),
Err(e) => {
tracing::warn!(error = %e, "DTO serialization failed in apply_select; returning empty object");
json!({})
}
}
}
_ => serde_json::to_value(value).unwrap_or_else(|e| {
tracing::warn!(error = %e, "DTO serialization failed in apply_select; returning empty object");
json!({})
}),
}
}
#[must_use]
pub fn page_to_projected_json<T: serde::Serialize>(
page: &modkit_odata::Page<T>,
selected_fields: Option<&[String]>,
) -> modkit_odata::Page<Value> {
let projected_items: Vec<Value> = page
.items
.iter()
.map(|item| apply_select(item, selected_fields))
.collect();
modkit_odata::Page {
items: projected_items,
page_info: page.page_info.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_project_json_object() {
let value = json!({
"id": "123",
"name": "John",
"email": "john@example.com",
"age": 30
});
let selected = ["id".to_owned(), "name".to_owned()];
let fields_set: HashSet<String> = selected.iter().map(|f| f.to_lowercase()).collect();
let projected = project_json(&value, &fields_set);
assert_eq!(projected.get("id").and_then(|v| v.as_str()), Some("123"));
assert_eq!(projected.get("name").and_then(|v| v.as_str()), Some("John"));
assert!(projected.get("email").is_none());
assert!(projected.get("age").is_none());
}
#[test]
fn test_project_json_case_insensitive() {
let value = json!({
"Id": "123",
"Name": "John"
});
let selected = ["id".to_owned(), "name".to_owned()];
let fields_set: HashSet<String> = selected.iter().map(|f| f.to_lowercase()).collect();
let projected = project_json(&value, &fields_set);
assert_eq!(projected.get("Id").and_then(|v| v.as_str()), Some("123"));
assert_eq!(projected.get("Name").and_then(|v| v.as_str()), Some("John"));
}
#[test]
fn test_project_json_array() {
let value = json!([
{"id": "1", "name": "John"},
{"id": "2", "name": "Jane"}
]);
let selected = ["id".to_owned()];
let fields_set: HashSet<String> = selected.iter().map(|f| f.to_lowercase()).collect();
let projected = project_json(&value, &fields_set);
let arr = projected.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0].get("id").and_then(|v| v.as_str()), Some("1"));
assert!(arr[0].get("name").is_none());
}
#[test]
fn test_project_json_nested() {
let value = json!({
"id": "123",
"user": {
"name": "John",
"email": "john@example.com"
}
});
let selected = ["id".to_owned(), "user".to_owned()];
let fields_set: HashSet<String> = selected.iter().map(|f| f.to_lowercase()).collect();
let projected = project_json(&value, &fields_set);
assert_eq!(projected.get("id").and_then(|v| v.as_str()), Some("123"));
assert!(projected.get("user").is_some());
}
#[test]
fn test_apply_select_with_fields() {
#[derive(serde::Serialize)]
struct User {
id: String,
name: String,
email: String,
}
let user = User {
id: "123".to_owned(),
name: "John".to_owned(),
email: "john@example.com".to_owned(),
};
let selected = vec!["id".to_owned(), "name".to_owned()];
let result = apply_select(&user, Some(&selected));
assert_eq!(result.get("id").and_then(|v| v.as_str()), Some("123"));
assert_eq!(result.get("name").and_then(|v| v.as_str()), Some("John"));
assert!(result.get("email").is_none());
}
#[test]
fn test_apply_select_without_fields() {
#[derive(serde::Serialize)]
struct User {
id: String,
name: String,
}
let user = User {
id: "123".to_owned(),
name: "John".to_owned(),
};
let result = apply_select(&user, None);
assert_eq!(result.get("id").and_then(|v| v.as_str()), Some("123"));
assert_eq!(result.get("name").and_then(|v| v.as_str()), Some("John"));
}
}