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(_) => json!({}),
100 }
101 }
102 _ => serde_json::to_value(value).unwrap_or_else(|_| json!({})),
103 }
104}
105
106#[must_use]
120pub fn page_to_projected_json<T: serde::Serialize>(
121 page: &modkit_odata::Page<T>,
122 selected_fields: Option<&[String]>,
123) -> modkit_odata::Page<Value> {
124 let projected_items: Vec<Value> = page
125 .items
126 .iter()
127 .map(|item| apply_select(item, selected_fields))
128 .collect();
129
130 modkit_odata::Page {
131 items: projected_items,
132 page_info: page.page_info.clone(),
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_project_json_object() {
142 let value = json!({
143 "id": "123",
144 "name": "John",
145 "email": "john@example.com",
146 "age": 30
147 });
148
149 let selected = ["id".to_owned(), "name".to_owned()];
150 let fields_set: HashSet<String> = selected.iter().map(|f| f.to_lowercase()).collect();
151
152 let projected = project_json(&value, &fields_set);
153
154 assert_eq!(projected.get("id").and_then(|v| v.as_str()), Some("123"));
155 assert_eq!(projected.get("name").and_then(|v| v.as_str()), Some("John"));
156 assert!(projected.get("email").is_none());
157 assert!(projected.get("age").is_none());
158 }
159
160 #[test]
161 fn test_project_json_case_insensitive() {
162 let value = json!({
163 "Id": "123",
164 "Name": "John"
165 });
166
167 let selected = ["id".to_owned(), "name".to_owned()];
168 let fields_set: HashSet<String> = selected.iter().map(|f| f.to_lowercase()).collect();
169
170 let projected = project_json(&value, &fields_set);
171
172 assert_eq!(projected.get("Id").and_then(|v| v.as_str()), Some("123"));
173 assert_eq!(projected.get("Name").and_then(|v| v.as_str()), Some("John"));
174 }
175
176 #[test]
177 fn test_project_json_array() {
178 let value = json!([
179 {"id": "1", "name": "John"},
180 {"id": "2", "name": "Jane"}
181 ]);
182
183 let selected = ["id".to_owned()];
184 let fields_set: HashSet<String> = selected.iter().map(|f| f.to_lowercase()).collect();
185
186 let projected = project_json(&value, &fields_set);
187
188 let arr = projected.as_array().unwrap();
189 assert_eq!(arr.len(), 2);
190 assert_eq!(arr[0].get("id").and_then(|v| v.as_str()), Some("1"));
191 assert!(arr[0].get("name").is_none());
192 }
193
194 #[test]
195 fn test_project_json_nested() {
196 let value = json!({
197 "id": "123",
198 "user": {
199 "name": "John",
200 "email": "john@example.com"
201 }
202 });
203
204 let selected = ["id".to_owned(), "user".to_owned()];
205 let fields_set: HashSet<String> = selected.iter().map(|f| f.to_lowercase()).collect();
206
207 let projected = project_json(&value, &fields_set);
208
209 assert_eq!(projected.get("id").and_then(|v| v.as_str()), Some("123"));
210 assert!(projected.get("user").is_some());
211 }
212
213 #[test]
214 fn test_apply_select_with_fields() {
215 #[derive(serde::Serialize)]
216 struct User {
217 id: String,
218 name: String,
219 email: String,
220 }
221
222 let user = User {
223 id: "123".to_owned(),
224 name: "John".to_owned(),
225 email: "john@example.com".to_owned(),
226 };
227
228 let selected = vec!["id".to_owned(), "name".to_owned()];
229 let result = apply_select(&user, Some(&selected));
230
231 assert_eq!(result.get("id").and_then(|v| v.as_str()), Some("123"));
232 assert_eq!(result.get("name").and_then(|v| v.as_str()), Some("John"));
233 assert!(result.get("email").is_none());
234 }
235
236 #[test]
237 fn test_apply_select_without_fields() {
238 #[derive(serde::Serialize)]
239 struct User {
240 id: String,
241 name: String,
242 }
243
244 let user = User {
245 id: "123".to_owned(),
246 name: "John".to_owned(),
247 };
248
249 let result = apply_select(&user, None);
250
251 assert_eq!(result.get("id").and_then(|v| v.as_str()), Some("123"));
252 assert_eq!(result.get("name").and_then(|v| v.as_str()), Some("John"));
253 }
254}