1use serde_json::{Map, Value as JsonValue};
4
5use crate::{
6 db::types::JsonbValue,
7 error::{FraiseQLError, Result},
8};
9
10#[derive(Debug, Clone)]
12pub struct FieldMapping {
13 pub source: String,
15 pub output: String,
17 pub nested_typename: Option<String>,
20 pub nested_fields: Option<Vec<FieldMapping>>,
22}
23
24impl FieldMapping {
25 #[must_use]
27 pub fn simple(name: impl Into<String>) -> Self {
28 let name = name.into();
29 Self {
30 source: name.clone(),
31 output: name,
32 nested_typename: None,
33 nested_fields: None,
34 }
35 }
36
37 #[must_use]
39 pub fn aliased(source: impl Into<String>, alias: impl Into<String>) -> Self {
40 Self {
41 source: source.into(),
42 output: alias.into(),
43 nested_typename: None,
44 nested_fields: None,
45 }
46 }
47
48 #[must_use]
60 pub fn nested_object(
61 name: impl Into<String>,
62 typename: impl Into<String>,
63 fields: Vec<FieldMapping>,
64 ) -> Self {
65 let name = name.into();
66 Self {
67 source: name.clone(),
68 output: name,
69 nested_typename: Some(typename.into()),
70 nested_fields: Some(fields),
71 }
72 }
73
74 #[must_use]
76 pub fn nested_object_aliased(
77 source: impl Into<String>,
78 alias: impl Into<String>,
79 typename: impl Into<String>,
80 fields: Vec<FieldMapping>,
81 ) -> Self {
82 Self {
83 source: source.into(),
84 output: alias.into(),
85 nested_typename: Some(typename.into()),
86 nested_fields: Some(fields),
87 }
88 }
89
90 #[must_use]
92 pub fn with_nested_typename(mut self, typename: impl Into<String>) -> Self {
93 self.nested_typename = Some(typename.into());
94 self
95 }
96
97 #[must_use]
99 pub fn with_nested_fields(mut self, fields: Vec<FieldMapping>) -> Self {
100 self.nested_fields = Some(fields);
101 self
102 }
103}
104
105#[derive(Debug, Clone)]
107pub struct ProjectionMapper {
108 pub fields: Vec<FieldMapping>,
110 pub typename: Option<String>,
112}
113
114impl ProjectionMapper {
115 #[must_use]
117 pub fn new(fields: Vec<String>) -> Self {
118 Self {
119 fields: fields.into_iter().map(FieldMapping::simple).collect(),
120 typename: None,
121 }
122 }
123
124 #[must_use]
126 pub fn with_mappings(fields: Vec<FieldMapping>) -> Self {
127 Self {
128 fields,
129 typename: None,
130 }
131 }
132
133 #[must_use]
135 pub fn with_typename(mut self, typename: impl Into<String>) -> Self {
136 self.typename = Some(typename.into());
137 self
138 }
139
140 pub fn project(&self, jsonb: &JsonbValue) -> Result<JsonValue> {
154 let value = jsonb.as_value();
156
157 match value {
158 JsonValue::Object(map) => self.project_json_object(map),
159 JsonValue::Array(arr) => self.project_json_array(arr),
160 v => Ok(v.clone()),
161 }
162 }
163
164 fn project_json_object(&self, map: &serde_json::Map<String, JsonValue>) -> Result<JsonValue> {
166 let mut result = Map::new();
167
168 if let Some(ref typename) = self.typename {
170 result.insert("__typename".to_string(), JsonValue::String(typename.clone()));
171 }
172
173 for field in &self.fields {
175 if let Some(value) = map.get(&field.source) {
176 let projected_value = self.project_nested_value(value, field)?;
178 result.insert(field.output.clone(), projected_value);
179 }
180 }
181
182 Ok(JsonValue::Object(result))
183 }
184
185 #[allow(clippy::only_used_in_recursion, clippy::self_only_used_in_recursion)] fn project_nested_value(&self, value: &JsonValue, field: &FieldMapping) -> Result<JsonValue> {
188 match value {
189 JsonValue::Object(obj) => {
190 if let Some(ref typename) = field.nested_typename {
192 let mut result = Map::new();
193 result.insert("__typename".to_string(), JsonValue::String(typename.clone()));
194
195 if let Some(ref nested_fields) = field.nested_fields {
197 for nested_field in nested_fields {
198 if let Some(nested_value) = obj.get(&nested_field.source) {
199 let projected =
200 self.project_nested_value(nested_value, nested_field)?;
201 result.insert(nested_field.output.clone(), projected);
202 }
203 }
204 } else {
205 for (k, v) in obj {
207 result.insert(k.clone(), v.clone());
208 }
209 }
210 Ok(JsonValue::Object(result))
211 } else {
212 Ok(value.clone())
214 }
215 },
216 JsonValue::Array(arr) => {
217 if field.nested_typename.is_some() {
219 let projected: Result<Vec<JsonValue>> =
220 arr.iter().map(|item| self.project_nested_value(item, field)).collect();
221 Ok(JsonValue::Array(projected?))
222 } else {
223 Ok(value.clone())
224 }
225 },
226 _ => {
227 if let JsonValue::String(ref s) = *value {
233 if let Ok(parsed @ (JsonValue::Object(_) | JsonValue::Array(_))) =
234 serde_json::from_str::<JsonValue>(s)
235 {
236 return self.project_nested_value(&parsed, field);
237 }
238 }
239 Ok(value.clone())
240 },
241 }
242 }
243
244 fn project_json_array(&self, arr: &[JsonValue]) -> Result<JsonValue> {
246 let projected: Vec<JsonValue> = arr
247 .iter()
248 .filter_map(|item| {
249 if let JsonValue::Object(obj) = item {
250 self.project_json_object(obj).ok()
251 } else {
252 Some(item.clone())
253 }
254 })
255 .collect();
256
257 Ok(JsonValue::Array(projected))
258 }
259}
260
261pub struct ResultProjector {
263 mapper: ProjectionMapper,
264}
265
266impl ResultProjector {
267 #[must_use]
269 pub fn new(fields: Vec<String>) -> Self {
270 Self {
271 mapper: ProjectionMapper::new(fields),
272 }
273 }
274
275 #[must_use]
277 pub fn with_mappings(fields: Vec<FieldMapping>) -> Self {
278 Self {
279 mapper: ProjectionMapper::with_mappings(fields),
280 }
281 }
282
283 #[must_use]
288 pub fn with_typename(mut self, typename: impl Into<String>) -> Self {
289 self.mapper = self.mapper.with_typename(typename);
290 self
291 }
292
293 pub fn project_results(&self, results: &[JsonbValue], is_list: bool) -> Result<JsonValue> {
308 if is_list {
309 let projected: Result<Vec<JsonValue>> =
311 results.iter().map(|r| self.mapper.project(r)).collect();
312
313 Ok(JsonValue::Array(projected?))
314 } else {
315 if let Some(first) = results.first() {
317 self.mapper.project(first)
318 } else {
319 Ok(JsonValue::Null)
320 }
321 }
322 }
323
324 #[must_use]
335 pub fn wrap_in_data_envelope(result: JsonValue, query_name: &str) -> JsonValue {
336 let mut data = Map::new();
337 data.insert(query_name.to_string(), result);
338
339 let mut response = Map::new();
340 response.insert("data".to_string(), JsonValue::Object(data));
341
342 JsonValue::Object(response)
343 }
344
345 pub fn add_typename_only(
372 &self,
373 projected_data: &JsonbValue,
374 typename: &str,
375 ) -> Result<JsonValue> {
376 let value = projected_data.as_value();
377
378 match value {
379 JsonValue::Object(map) => {
380 let mut result = map.clone();
381 result.insert("__typename".to_string(), JsonValue::String(typename.to_string()));
382 Ok(JsonValue::Object(result))
383 },
384 JsonValue::Array(arr) => {
385 let updated: Result<Vec<JsonValue>> = arr
386 .iter()
387 .map(|item| {
388 if let JsonValue::Object(obj) = item {
389 let mut result = obj.clone();
390 result.insert(
391 "__typename".to_string(),
392 JsonValue::String(typename.to_string()),
393 );
394 Ok(JsonValue::Object(result))
395 } else {
396 Ok(item.clone())
397 }
398 })
399 .collect();
400 Ok(JsonValue::Array(updated?))
401 },
402 v => Ok(v.clone()),
403 }
404 }
405
406 #[must_use]
416 pub fn wrap_error(error: &FraiseQLError) -> JsonValue {
417 let mut error_obj = Map::new();
418 error_obj.insert("message".to_string(), JsonValue::String(error.to_string()));
419
420 let mut response = Map::new();
421 response.insert("errors".to_string(), JsonValue::Array(vec![JsonValue::Object(error_obj)]));
422
423 JsonValue::Object(response)
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use serde_json::json;
430
431 use super::*;
432
433 #[test]
434 fn test_projection_mapper_new() {
435 let mapper = ProjectionMapper::new(vec!["id".to_string(), "name".to_string()]);
436 assert_eq!(mapper.fields.len(), 2);
437 }
438
439 #[test]
440 fn test_project_object() {
441 let mapper = ProjectionMapper::new(vec!["id".to_string(), "name".to_string()]);
442
443 let data = json!({
444 "id": "123",
445 "name": "Alice",
446 "email": "alice@example.com"
447 });
448
449 let jsonb = JsonbValue::new(data);
450 let result = mapper.project(&jsonb).unwrap();
451
452 assert_eq!(result, json!({ "id": "123", "name": "Alice" }));
453 }
454
455 #[test]
456 fn test_project_array() {
457 let mapper = ProjectionMapper::new(vec!["id".to_string()]);
458
459 let data = json!([
460 { "id": "1", "name": "Alice" },
461 { "id": "2", "name": "Bob" }
462 ]);
463
464 let jsonb = JsonbValue::new(data);
465 let result = mapper.project(&jsonb).unwrap();
466
467 assert_eq!(result, json!([{ "id": "1" }, { "id": "2" }]));
468 }
469
470 #[test]
471 fn test_result_projector_list() {
472 let projector = ResultProjector::new(vec!["id".to_string()]);
473
474 let data = json!({ "id": "1", "name": "Alice" });
475 let results = vec![JsonbValue::new(data)];
476 let result = projector.project_results(&results, true).unwrap();
477
478 assert_eq!(result, json!([{ "id": "1" }]));
479 }
480
481 #[test]
482 fn test_result_projector_single() {
483 let projector = ResultProjector::new(vec!["id".to_string()]);
484
485 let data = json!({ "id": "1", "name": "Alice" });
486 let results = vec![JsonbValue::new(data)];
487 let result = projector.project_results(&results, false).unwrap();
488
489 assert_eq!(result, json!({ "id": "1" }));
490 }
491
492 #[test]
493 fn test_wrap_in_data_envelope() {
494 let result = json!([{ "id": "1" }]);
495 let wrapped = ResultProjector::wrap_in_data_envelope(result, "users");
496
497 assert_eq!(wrapped, json!({ "data": { "users": [{ "id": "1" }] } }));
498 }
499
500 #[test]
501 fn test_wrap_error() {
502 let error = FraiseQLError::Validation {
503 message: "Invalid query".to_string(),
504 path: None,
505 };
506
507 let wrapped = ResultProjector::wrap_error(&error);
508
509 assert!(wrapped.get("errors").is_some());
510 assert_eq!(wrapped.get("data"), None);
511 }
512
513 #[test]
514 fn test_add_typename_only_object() {
515 let projector = ResultProjector::new(vec!["id".to_string()]);
516
517 let data = json!({ "id": "123", "name": "Alice" });
518 let jsonb = JsonbValue::new(data);
519 let result = projector.add_typename_only(&jsonb, "User").unwrap();
520
521 assert_eq!(result, json!({ "id": "123", "name": "Alice", "__typename": "User" }));
522 }
523
524 #[test]
525 fn test_add_typename_only_array() {
526 let projector = ResultProjector::new(vec!["id".to_string()]);
527
528 let data = json!([
529 { "id": "1", "name": "Alice" },
530 { "id": "2", "name": "Bob" }
531 ]);
532 let jsonb = JsonbValue::new(data);
533 let result = projector.add_typename_only(&jsonb, "User").unwrap();
534
535 assert_eq!(
536 result,
537 json!([
538 { "id": "1", "name": "Alice", "__typename": "User" },
539 { "id": "2", "name": "Bob", "__typename": "User" }
540 ])
541 );
542 }
543
544 #[test]
545 fn test_add_typename_only_primitive() {
546 let projector = ResultProjector::new(vec![]);
547
548 let jsonb = JsonbValue::new(json!("string_value"));
549 let result = projector.add_typename_only(&jsonb, "String").unwrap();
550
551 assert_eq!(result, json!("string_value"));
553 }
554
555 #[test]
560 fn test_field_mapping_simple() {
561 let mapping = FieldMapping::simple("name");
562 assert_eq!(mapping.source, "name");
563 assert_eq!(mapping.output, "name");
564 }
565
566 #[test]
567 fn test_field_mapping_aliased() {
568 let mapping = FieldMapping::aliased("author", "writer");
569 assert_eq!(mapping.source, "author");
570 assert_eq!(mapping.output, "writer");
571 }
572
573 #[test]
574 fn test_project_with_alias() {
575 let mapper = ProjectionMapper::with_mappings(vec![
576 FieldMapping::simple("id"),
577 FieldMapping::aliased("author", "writer"),
578 ]);
579
580 let data = json!({
581 "id": "123",
582 "author": { "name": "Alice" },
583 "title": "Hello World"
584 });
585
586 let jsonb = JsonbValue::new(data);
587 let result = mapper.project(&jsonb).unwrap();
588
589 assert_eq!(
591 result,
592 json!({
593 "id": "123",
594 "writer": { "name": "Alice" }
595 })
596 );
597 }
598
599 #[test]
600 fn test_project_with_typename() {
601 let mapper =
602 ProjectionMapper::new(vec!["id".to_string(), "name".to_string()]).with_typename("User");
603
604 let data = json!({
605 "id": "123",
606 "name": "Alice",
607 "email": "alice@example.com"
608 });
609
610 let jsonb = JsonbValue::new(data);
611 let result = mapper.project(&jsonb).unwrap();
612
613 assert_eq!(
614 result,
615 json!({
616 "__typename": "User",
617 "id": "123",
618 "name": "Alice"
619 })
620 );
621 }
622
623 #[test]
624 fn test_project_with_alias_and_typename() {
625 let mapper = ProjectionMapper::with_mappings(vec![
626 FieldMapping::simple("id"),
627 FieldMapping::aliased("author", "writer"),
628 ])
629 .with_typename("Post");
630
631 let data = json!({
632 "id": "post-1",
633 "author": { "name": "Alice" },
634 "title": "Hello"
635 });
636
637 let jsonb = JsonbValue::new(data);
638 let result = mapper.project(&jsonb).unwrap();
639
640 assert_eq!(
641 result,
642 json!({
643 "__typename": "Post",
644 "id": "post-1",
645 "writer": { "name": "Alice" }
646 })
647 );
648 }
649
650 #[test]
651 fn test_result_projector_with_typename() {
652 let projector =
653 ResultProjector::new(vec!["id".to_string(), "name".to_string()]).with_typename("User");
654
655 let data = json!({ "id": "1", "name": "Alice", "email": "alice@example.com" });
656 let results = vec![JsonbValue::new(data)];
657 let result = projector.project_results(&results, false).unwrap();
658
659 assert_eq!(
660 result,
661 json!({
662 "__typename": "User",
663 "id": "1",
664 "name": "Alice"
665 })
666 );
667 }
668
669 #[test]
670 fn test_result_projector_list_with_typename() {
671 let projector = ResultProjector::new(vec!["id".to_string()]).with_typename("User");
672
673 let results = vec![
674 JsonbValue::new(json!({ "id": "1", "name": "Alice" })),
675 JsonbValue::new(json!({ "id": "2", "name": "Bob" })),
676 ];
677 let result = projector.project_results(&results, true).unwrap();
678
679 assert_eq!(
680 result,
681 json!([
682 { "__typename": "User", "id": "1" },
683 { "__typename": "User", "id": "2" }
684 ])
685 );
686 }
687
688 #[test]
689 fn test_result_projector_with_mappings() {
690 let projector = ResultProjector::with_mappings(vec![
691 FieldMapping::simple("id"),
692 FieldMapping::aliased("full_name", "name"),
693 ]);
694
695 let data = json!({ "id": "1", "full_name": "Alice Smith", "email": "alice@example.com" });
696 let results = vec![JsonbValue::new(data)];
697 let result = projector.project_results(&results, false).unwrap();
698
699 assert_eq!(
701 result,
702 json!({
703 "id": "1",
704 "name": "Alice Smith"
705 })
706 );
707 }
708
709 #[test]
714 fn test_nested_object_typename() {
715 let mapper = ProjectionMapper::with_mappings(vec![
716 FieldMapping::simple("id"),
717 FieldMapping::simple("title"),
718 FieldMapping::nested_object(
719 "author",
720 "User",
721 vec![FieldMapping::simple("id"), FieldMapping::simple("name")],
722 ),
723 ])
724 .with_typename("Post");
725
726 let data = json!({
727 "id": "post-1",
728 "title": "Hello World",
729 "author": {
730 "id": "user-1",
731 "name": "Alice",
732 "email": "alice@example.com"
733 }
734 });
735
736 let jsonb = JsonbValue::new(data);
737 let result = mapper.project(&jsonb).unwrap();
738
739 assert_eq!(
740 result,
741 json!({
742 "__typename": "Post",
743 "id": "post-1",
744 "title": "Hello World",
745 "author": {
746 "__typename": "User",
747 "id": "user-1",
748 "name": "Alice"
749 }
750 })
751 );
752 }
753
754 #[test]
755 fn test_nested_array_typename() {
756 let mapper = ProjectionMapper::with_mappings(vec![
757 FieldMapping::simple("id"),
758 FieldMapping::simple("name"),
759 FieldMapping::nested_object(
760 "posts",
761 "Post",
762 vec![FieldMapping::simple("id"), FieldMapping::simple("title")],
763 ),
764 ])
765 .with_typename("User");
766
767 let data = json!({
768 "id": "user-1",
769 "name": "Alice",
770 "posts": [
771 { "id": "post-1", "title": "First Post", "views": 100 },
772 { "id": "post-2", "title": "Second Post", "views": 200 }
773 ]
774 });
775
776 let jsonb = JsonbValue::new(data);
777 let result = mapper.project(&jsonb).unwrap();
778
779 assert_eq!(
780 result,
781 json!({
782 "__typename": "User",
783 "id": "user-1",
784 "name": "Alice",
785 "posts": [
786 { "__typename": "Post", "id": "post-1", "title": "First Post" },
787 { "__typename": "Post", "id": "post-2", "title": "Second Post" }
788 ]
789 })
790 );
791 }
792
793 #[test]
794 fn test_deeply_nested_typename() {
795 let mapper = ProjectionMapper::with_mappings(vec![
797 FieldMapping::simple("id"),
798 FieldMapping::nested_object(
799 "author",
800 "User",
801 vec![
802 FieldMapping::simple("name"),
803 FieldMapping::nested_object(
804 "company",
805 "Company",
806 vec![FieldMapping::simple("name")],
807 ),
808 ],
809 ),
810 ])
811 .with_typename("Post");
812
813 let data = json!({
814 "id": "post-1",
815 "author": {
816 "name": "Alice",
817 "company": {
818 "name": "Acme Corp",
819 "revenue": 1_000_000
820 }
821 }
822 });
823
824 let jsonb = JsonbValue::new(data);
825 let result = mapper.project(&jsonb).unwrap();
826
827 assert_eq!(
828 result,
829 json!({
830 "__typename": "Post",
831 "id": "post-1",
832 "author": {
833 "__typename": "User",
834 "name": "Alice",
835 "company": {
836 "__typename": "Company",
837 "name": "Acme Corp"
838 }
839 }
840 })
841 );
842 }
843
844 #[test]
845 fn test_nested_object_with_alias_and_typename() {
846 let mapper = ProjectionMapper::with_mappings(vec![
847 FieldMapping::simple("id"),
848 FieldMapping::nested_object_aliased(
849 "author",
850 "writer",
851 "User",
852 vec![FieldMapping::simple("id"), FieldMapping::simple("name")],
853 ),
854 ])
855 .with_typename("Post");
856
857 let data = json!({
858 "id": "post-1",
859 "author": {
860 "id": "user-1",
861 "name": "Alice"
862 }
863 });
864
865 let jsonb = JsonbValue::new(data);
866 let result = mapper.project(&jsonb).unwrap();
867
868 assert_eq!(
870 result,
871 json!({
872 "__typename": "Post",
873 "id": "post-1",
874 "writer": {
875 "__typename": "User",
876 "id": "user-1",
877 "name": "Alice"
878 }
879 })
880 );
881 }
882
883 #[test]
888 fn test_nested_object_as_json_string_is_re_parsed() {
889 let mapper = ProjectionMapper::with_mappings(vec![
893 FieldMapping::simple("id"),
894 FieldMapping::nested_object(
895 "author",
896 "User",
897 vec![FieldMapping::simple("id"), FieldMapping::simple("name")],
898 ),
899 ])
900 .with_typename("Post");
901
902 let data = json!({
904 "id": "post-1",
905 "author": "{\"id\":\"user-2\",\"name\":\"Bob\"}"
906 });
907
908 let jsonb = JsonbValue::new(data);
909 let result = mapper.project(&jsonb).unwrap();
910
911 let author = result.get("author").expect("author field missing");
913 assert!(author.is_object(), "author should be a JSON object, got: {:?}", author);
914 assert_eq!(author.get("id"), Some(&json!("user-2")));
915 assert_eq!(author.get("name"), Some(&json!("Bob")));
916 }
917
918 #[test]
919 fn test_nested_without_specific_fields() {
920 let mapper = ProjectionMapper::with_mappings(vec![
922 FieldMapping::simple("id"),
923 FieldMapping::simple("author").with_nested_typename("User"),
924 ])
925 .with_typename("Post");
926
927 let data = json!({
928 "id": "post-1",
929 "author": {
930 "id": "user-1",
931 "name": "Alice",
932 "email": "alice@example.com"
933 }
934 });
935
936 let jsonb = JsonbValue::new(data);
937 let result = mapper.project(&jsonb).unwrap();
938
939 assert_eq!(
941 result,
942 json!({
943 "__typename": "Post",
944 "id": "post-1",
945 "author": {
946 "__typename": "User",
947 "id": "user-1",
948 "name": "Alice",
949 "email": "alice@example.com"
950 }
951 })
952 );
953 }
954}