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