1use serde_json::{Map, Value, json};
7use std::collections::HashSet;
8
9#[allow(clippy::implicit_hasher)] #[must_use]
39pub fn project_json(value: &Value, selected_fields: &HashSet<String>) -> Value {
40 match value {
41 Value::Object(map) => {
42 let mut projected = Map::new();
43 for (key, val) in map {
44 let key_lower = key.to_lowercase();
45
46 if selected_fields.contains(&key_lower) {
48 projected.insert(key.clone(), val.clone());
50 } else {
51 let nested_fields = extract_nested_fields(&key_lower, selected_fields);
53 if !nested_fields.is_empty() {
54 projected.insert(key.clone(), project_json(val, &nested_fields));
56 }
57 }
58 }
59 Value::Object(projected)
60 }
61 Value::Array(arr) => Value::Array(
62 arr.iter()
63 .map(|v| project_json(v, selected_fields))
64 .collect(),
65 ),
66 other => other.clone(),
67 }
68}
69
70fn extract_nested_fields(parent_key: &str, selected_fields: &HashSet<String>) -> HashSet<String> {
75 let prefix = format!("{parent_key}.");
76 selected_fields
77 .iter()
78 .filter(|field| field.starts_with(&prefix))
79 .map(|field| field[prefix.len()..].to_string())
80 .collect()
81}
82
83pub fn apply_select<T: serde::Serialize>(value: T, selected_fields: Option<&[String]>) -> Value {
94 match selected_fields {
95 Some(fields) if !fields.is_empty() => {
96 let fields_set: HashSet<String> = fields.iter().map(|f| f.to_lowercase()).collect();
97 match serde_json::to_value(value) {
98 Ok(v) => project_json(&v, &fields_set),
99 Err(e) => {
100 tracing::warn!(error = %e, "DTO serialization failed in apply_select; returning empty object");
101 json!({})
102 }
103 }
104 }
105 _ => serde_json::to_value(value).unwrap_or_else(|e| {
106 tracing::warn!(error = %e, "DTO serialization failed in apply_select; returning empty object");
107 json!({})
108 }),
109 }
110}
111
112#[must_use]
126pub fn page_to_projected_json<T: serde::Serialize>(
127 page: &modkit_odata::Page<T>,
128 selected_fields: Option<&[String]>,
129) -> modkit_odata::Page<Value> {
130 let projected_items: Vec<Value> = page
131 .items
132 .iter()
133 .map(|item| apply_select(item, selected_fields))
134 .collect();
135
136 modkit_odata::Page {
137 items: projected_items,
138 page_info: page.page_info.clone(),
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_project_json_object() {
148 let value = json!({
149 "id": "123",
150 "name": "John",
151 "email": "john@example.com",
152 "age": 30
153 });
154
155 let selected = ["id".to_owned(), "name".to_owned()];
156 let fields_set: HashSet<String> = selected.iter().map(|f| f.to_lowercase()).collect();
157
158 let projected = project_json(&value, &fields_set);
159
160 assert_eq!(projected.get("id").and_then(|v| v.as_str()), Some("123"));
161 assert_eq!(projected.get("name").and_then(|v| v.as_str()), Some("John"));
162 assert!(projected.get("email").is_none());
163 assert!(projected.get("age").is_none());
164 }
165
166 #[test]
167 fn test_project_json_case_insensitive() {
168 let value = json!({
169 "Id": "123",
170 "Name": "John"
171 });
172
173 let selected = ["id".to_owned(), "name".to_owned()];
174 let fields_set: HashSet<String> = selected.iter().map(|f| f.to_lowercase()).collect();
175
176 let projected = project_json(&value, &fields_set);
177
178 assert_eq!(projected.get("Id").and_then(|v| v.as_str()), Some("123"));
179 assert_eq!(projected.get("Name").and_then(|v| v.as_str()), Some("John"));
180 }
181
182 #[test]
183 fn test_project_json_array() {
184 let value = json!([
185 {"id": "1", "name": "John"},
186 {"id": "2", "name": "Jane"}
187 ]);
188
189 let selected = ["id".to_owned()];
190 let fields_set: HashSet<String> = selected.iter().map(|f| f.to_lowercase()).collect();
191
192 let projected = project_json(&value, &fields_set);
193
194 let arr = projected.as_array().unwrap();
195 assert_eq!(arr.len(), 2);
196 assert_eq!(arr[0].get("id").and_then(|v| v.as_str()), Some("1"));
197 assert!(arr[0].get("name").is_none());
198 }
199
200 #[test]
201 fn test_project_json_nested() {
202 let value = json!({
203 "id": "123",
204 "user": {
205 "name": "John",
206 "email": "john@example.com"
207 }
208 });
209
210 let selected = ["id".to_owned(), "user".to_owned()];
211 let fields_set: HashSet<String> = selected.iter().map(|f| f.to_lowercase()).collect();
212
213 let projected = project_json(&value, &fields_set);
214
215 assert_eq!(projected.get("id").and_then(|v| v.as_str()), Some("123"));
216 assert!(projected.get("user").is_some());
217 }
218
219 #[test]
220 fn test_apply_select_with_fields() {
221 #[derive(serde::Serialize)]
222 struct User {
223 id: String,
224 name: String,
225 email: String,
226 }
227
228 let user = User {
229 id: "123".to_owned(),
230 name: "John".to_owned(),
231 email: "john@example.com".to_owned(),
232 };
233
234 let selected = vec!["id".to_owned(), "name".to_owned()];
235 let result = apply_select(&user, Some(&selected));
236
237 assert_eq!(result.get("id").and_then(|v| v.as_str()), Some("123"));
238 assert_eq!(result.get("name").and_then(|v| v.as_str()), Some("John"));
239 assert!(result.get("email").is_none());
240 }
241
242 #[test]
243 fn test_apply_select_without_fields() {
244 #[derive(serde::Serialize)]
245 struct User {
246 id: String,
247 name: String,
248 }
249
250 let user = User {
251 id: "123".to_owned(),
252 name: "John".to_owned(),
253 };
254
255 let result = apply_select(&user, None);
256
257 assert_eq!(result.get("id").and_then(|v| v.as_str()), Some("123"));
258 assert_eq!(result.get("name").and_then(|v| v.as_str()), Some("John"));
259 }
260}