1use std::collections::HashSet;
4
5use serde_json::{Map, Value as JsonValue};
6
7use crate::{
8 db::types::JsonbValue,
9 error::{FraiseQLError, Result},
10 graphql::FieldSelection,
11 schema::{CompiledSchema, FieldDefinition},
12};
13
14#[derive(Debug, Clone)]
16pub struct FieldMapping {
17 pub source: String,
19 pub output: String,
21 pub source_fallback: Option<String>,
25 pub nested_typename: Option<String>,
28 pub nested_fields: Option<Vec<FieldMapping>>,
30}
31
32impl FieldMapping {
33 #[must_use]
35 pub fn simple(name: impl Into<String>) -> Self {
36 let name = name.into();
37 Self {
38 source: name.clone(),
39 output: name,
40 source_fallback: None,
41 nested_typename: None,
42 nested_fields: None,
43 }
44 }
45
46 #[must_use]
48 pub fn aliased(source: impl Into<String>, alias: impl Into<String>) -> Self {
49 Self {
50 source: source.into(),
51 output: alias.into(),
52 source_fallback: None,
53 nested_typename: None,
54 nested_fields: None,
55 }
56 }
57
58 #[must_use]
72 pub fn nested_object(
73 name: impl Into<String>,
74 typename: impl Into<String>,
75 fields: Vec<FieldMapping>,
76 ) -> Self {
77 let name = name.into();
78 Self {
79 source: name.clone(),
80 output: name,
81 source_fallback: None,
82 nested_typename: Some(typename.into()),
83 nested_fields: Some(fields),
84 }
85 }
86
87 #[must_use]
89 pub fn nested_object_aliased(
90 source: impl Into<String>,
91 alias: impl Into<String>,
92 typename: impl Into<String>,
93 fields: Vec<FieldMapping>,
94 ) -> Self {
95 Self {
96 source: source.into(),
97 output: alias.into(),
98 source_fallback: None,
99 nested_typename: Some(typename.into()),
100 nested_fields: Some(fields),
101 }
102 }
103
104 #[must_use]
106 pub fn with_nested_typename(mut self, typename: impl Into<String>) -> Self {
107 self.nested_typename = Some(typename.into());
108 self
109 }
110
111 #[must_use]
113 pub fn with_nested_fields(mut self, fields: Vec<FieldMapping>) -> Self {
114 self.nested_fields = Some(fields);
115 self
116 }
117}
118
119#[derive(Debug, Clone)]
121pub struct ProjectionMapper {
122 pub fields: Vec<FieldMapping>,
124 pub typename: Option<String>,
126 pub federation_mode: bool,
129}
130
131impl ProjectionMapper {
132 #[must_use]
134 pub fn new(fields: Vec<String>) -> Self {
135 Self {
136 fields: fields.into_iter().map(FieldMapping::simple).collect(),
137 typename: None,
138 federation_mode: false,
139 }
140 }
141
142 #[must_use]
144 pub const fn with_mappings(fields: Vec<FieldMapping>) -> Self {
145 Self {
146 fields,
147 typename: None,
148 federation_mode: false,
149 }
150 }
151
152 #[must_use]
154 pub fn with_typename(mut self, typename: impl Into<String>) -> Self {
155 self.typename = Some(typename.into());
156 self
157 }
158
159 #[must_use]
161 pub const fn with_federation_mode(mut self, enabled: bool) -> Self {
162 self.federation_mode = enabled;
163 self
164 }
165
166 pub fn project(&self, jsonb: &JsonbValue) -> Result<JsonValue> {
180 let value = jsonb.as_value();
182
183 match value {
184 JsonValue::Object(map) => self.project_json_object(map),
185 JsonValue::Array(arr) => self.project_json_array(arr),
186 v => Ok(v.clone()),
187 }
188 }
189
190 pub fn project_json_object(
200 &self,
201 map: &serde_json::Map<String, JsonValue>,
202 ) -> Result<JsonValue> {
203 let mut result = Map::new();
204
205 if let Some(ref typename) = self.typename {
207 result.insert("__typename".to_string(), JsonValue::String(typename.clone()));
208 }
209
210 for field in &self.fields {
212 let value = map
213 .get(&field.source)
214 .or_else(|| field.source_fallback.as_ref().and_then(|fb| map.get(fb)));
215 if let Some(value) = value {
216 let projected_value = self.project_nested_value(value, field)?;
217 result.insert(field.output.clone(), projected_value);
218 }
219 }
220
221 Ok(JsonValue::Object(result))
222 }
223
224 #[allow(clippy::self_only_used_in_recursion)] fn project_nested_value(&self, value: &JsonValue, field: &FieldMapping) -> Result<JsonValue> {
227 match value {
228 JsonValue::Object(obj) => {
229 if let Some(ref typename) = field.nested_typename {
231 let mut result = Map::new();
232 result.insert("__typename".to_string(), JsonValue::String(typename.clone()));
233
234 if let Some(ref nested_fields) = field.nested_fields {
236 for nested_field in nested_fields {
237 if let Some(nested_value) = obj.get(&nested_field.source) {
238 let projected =
239 self.project_nested_value(nested_value, nested_field)?;
240 result.insert(nested_field.output.clone(), projected);
241 }
242 }
243 } else {
244 for (k, v) in obj {
246 result.insert(k.clone(), v.clone());
247 }
248 }
249 Ok(JsonValue::Object(result))
250 } else {
251 Ok(value.clone())
253 }
254 },
255 JsonValue::Array(arr) => {
256 if field.nested_typename.is_some() {
258 let projected: Result<Vec<JsonValue>> =
259 arr.iter().map(|item| self.project_nested_value(item, field)).collect();
260 Ok(JsonValue::Array(projected?))
261 } else {
262 Ok(value.clone())
263 }
264 },
265 _ => {
266 if let JsonValue::String(ref s) = *value {
272 if let Ok(parsed @ (JsonValue::Object(_) | JsonValue::Array(_))) =
273 serde_json::from_str::<JsonValue>(s)
274 {
275 return self.project_nested_value(&parsed, field);
276 }
277 }
278 Ok(value.clone())
279 },
280 }
281 }
282
283 fn project_json_array(&self, arr: &[JsonValue]) -> Result<JsonValue> {
285 let projected: Vec<JsonValue> = arr
286 .iter()
287 .filter_map(|item| {
288 if let JsonValue::Object(obj) = item {
289 self.project_json_object(obj).ok()
290 } else {
291 Some(item.clone())
292 }
293 })
294 .collect();
295
296 Ok(JsonValue::Array(projected))
297 }
298}
299
300pub struct ResultProjector {
302 mapper: ProjectionMapper,
303}
304
305impl ResultProjector {
306 #[must_use]
308 pub fn new(fields: Vec<String>) -> Self {
309 Self {
310 mapper: ProjectionMapper::new(fields),
311 }
312 }
313
314 #[must_use]
316 pub const fn with_mappings(fields: Vec<FieldMapping>) -> Self {
317 Self {
318 mapper: ProjectionMapper::with_mappings(fields),
319 }
320 }
321
322 #[must_use]
327 pub fn with_typename(mut self, typename: impl Into<String>) -> Self {
328 self.mapper = self.mapper.with_typename(typename);
329 self
330 }
331
332 #[must_use]
337 pub fn configure_typename_from_selections(
338 self,
339 selections: &[FieldSelection],
340 entity_type: &str,
341 ) -> Self {
342 let wants_typename = selections
343 .first()
344 .is_some_and(|root| root.nested_fields.iter().any(|f| f.name == "__typename"));
345 if wants_typename {
346 self.with_typename(entity_type)
347 } else {
348 self
349 }
350 }
351
352 #[must_use]
357 pub fn with_federation_mode(mut self, enabled: bool) -> Self {
358 self.mapper = self.mapper.with_federation_mode(enabled);
359 self
360 }
361
362 pub fn project_results(&self, results: &[JsonbValue], is_list: bool) -> Result<JsonValue> {
377 if is_list {
378 let projected: Result<Vec<JsonValue>> =
380 results.iter().map(|r| self.mapper.project(r)).collect();
381
382 Ok(JsonValue::Array(projected?))
383 } else {
384 if let Some(first) = results.first() {
386 self.mapper.project(first)
387 } else {
388 Ok(JsonValue::Null)
389 }
390 }
391 }
392
393 #[must_use]
404 pub fn wrap_in_data_envelope(result: JsonValue, query_name: &str) -> JsonValue {
405 let mut data = Map::new();
406 data.insert(query_name.to_string(), result);
407
408 let mut response = Map::new();
409 response.insert("data".to_string(), JsonValue::Object(data));
410
411 JsonValue::Object(response)
412 }
413
414 pub fn add_typename_only(
451 &self,
452 projected_data: &JsonbValue,
453 typename: &str,
454 ) -> Result<JsonValue> {
455 let value = projected_data.as_value();
456
457 match value {
458 JsonValue::Object(map) => {
459 let mut result = map.clone();
460 result.insert("__typename".to_string(), JsonValue::String(typename.to_string()));
461 Ok(JsonValue::Object(result))
462 },
463 JsonValue::Array(arr) => {
464 let updated: Result<Vec<JsonValue>> = arr
465 .iter()
466 .map(|item| {
467 if let JsonValue::Object(obj) = item {
468 let mut result = obj.clone();
469 result.insert(
470 "__typename".to_string(),
471 JsonValue::String(typename.to_string()),
472 );
473 Ok(JsonValue::Object(result))
474 } else {
475 Ok(item.clone())
476 }
477 })
478 .collect();
479 Ok(JsonValue::Array(updated?))
480 },
481 v => Ok(v.clone()),
482 }
483 }
484
485 #[must_use]
495 pub fn wrap_error(error: &FraiseQLError) -> JsonValue {
496 let mut error_obj = Map::new();
497 error_obj.insert("message".to_string(), JsonValue::String(error.to_string()));
498
499 let mut response = Map::new();
500 response.insert("errors".to_string(), JsonValue::Array(vec![JsonValue::Object(error_obj)]));
501
502 JsonValue::Object(response)
503 }
504}
505
506#[must_use]
521#[allow(clippy::implicit_hasher)] pub fn build_field_mappings_from_type(
523 fields: &[FieldDefinition],
524 schema: &CompiledSchema,
525 requested: Option<&[String]>,
526 visited: &mut HashSet<String>,
527) -> Vec<FieldMapping> {
528 fields
529 .iter()
530 .filter(|f| requested.is_none_or(|r| r.iter().any(|name| name == f.name.as_str())))
531 .map(|field| {
532 let source = to_camel_case(field.name.as_str());
533 let output = field.name.to_string();
534
535 let source_fallback = if source != output {
538 Some(output.clone())
539 } else {
540 None
541 };
542
543 let inner = field.field_type.inner_type().unwrap_or(&field.field_type);
545
546 if let Some(type_name) = inner.type_name() {
547 if let Some(td) = schema.find_type(type_name) {
549 if visited.insert(type_name.to_string()) {
550 let nested =
551 build_field_mappings_from_type(&td.fields, schema, None, visited);
552 visited.remove(type_name);
553 return FieldMapping {
554 source,
555 output,
556 source_fallback,
557 nested_typename: Some(type_name.to_string()),
558 nested_fields: Some(nested),
559 };
560 }
561 }
563 }
564
565 FieldMapping {
566 source,
567 output,
568 source_fallback,
569 nested_typename: None,
570 nested_fields: None,
571 }
572 })
573 .collect()
574}
575
576fn to_camel_case(snake: &str) -> String {
581 let mut result = String::with_capacity(snake.len());
582 let mut capitalise_next = false;
583
584 for ch in snake.chars() {
585 if ch == '_' {
586 capitalise_next = true;
587 } else if capitalise_next {
588 result.push(ch.to_ascii_uppercase());
589 capitalise_next = false;
590 } else {
591 result.push(ch);
592 }
593 }
594
595 result
596}
597
598#[cfg(test)]
599mod tests {
600 #![allow(clippy::unwrap_used)] use serde_json::json;
603
604 use super::*;
605
606 #[test]
607 fn test_projection_mapper_new() {
608 let mapper = ProjectionMapper::new(vec!["id".to_string(), "name".to_string()]);
609 assert_eq!(mapper.fields.len(), 2);
610 }
611
612 #[test]
613 fn test_project_object() {
614 let mapper = ProjectionMapper::new(vec!["id".to_string(), "name".to_string()]);
615
616 let data = json!({
617 "id": "123",
618 "name": "Alice",
619 "email": "alice@example.com"
620 });
621
622 let jsonb = JsonbValue::new(data);
623 let result = mapper.project(&jsonb).unwrap();
624
625 assert_eq!(result, json!({ "id": "123", "name": "Alice" }));
626 }
627
628 #[test]
629 fn test_project_array() {
630 let mapper = ProjectionMapper::new(vec!["id".to_string()]);
631
632 let data = json!([
633 { "id": "1", "name": "Alice" },
634 { "id": "2", "name": "Bob" }
635 ]);
636
637 let jsonb = JsonbValue::new(data);
638 let result = mapper.project(&jsonb).unwrap();
639
640 assert_eq!(result, json!([{ "id": "1" }, { "id": "2" }]));
641 }
642
643 #[test]
644 fn test_result_projector_list() {
645 let projector = ResultProjector::new(vec!["id".to_string()]);
646
647 let data = json!({ "id": "1", "name": "Alice" });
648 let results = vec![JsonbValue::new(data)];
649 let result = projector.project_results(&results, true).unwrap();
650
651 assert_eq!(result, json!([{ "id": "1" }]));
652 }
653
654 #[test]
655 fn test_result_projector_single() {
656 let projector = ResultProjector::new(vec!["id".to_string()]);
657
658 let data = json!({ "id": "1", "name": "Alice" });
659 let results = vec![JsonbValue::new(data)];
660 let result = projector.project_results(&results, false).unwrap();
661
662 assert_eq!(result, json!({ "id": "1" }));
663 }
664
665 #[test]
666 fn test_wrap_in_data_envelope() {
667 let result = json!([{ "id": "1" }]);
668 let wrapped = ResultProjector::wrap_in_data_envelope(result, "users");
669
670 assert_eq!(wrapped, json!({ "data": { "users": [{ "id": "1" }] } }));
671 }
672
673 #[test]
674 fn test_wrap_error() {
675 let error = FraiseQLError::Validation {
676 message: "Invalid query".to_string(),
677 path: None,
678 };
679
680 let wrapped = ResultProjector::wrap_error(&error);
681
682 assert!(wrapped.get("errors").is_some());
683 assert_eq!(wrapped.get("data"), None);
684 }
685
686 #[test]
687 fn test_add_typename_only_object() {
688 let projector = ResultProjector::new(vec!["id".to_string()]);
689
690 let data = json!({ "id": "123", "name": "Alice" });
691 let jsonb = JsonbValue::new(data);
692 let result = projector.add_typename_only(&jsonb, "User").unwrap();
693
694 assert_eq!(result, json!({ "id": "123", "name": "Alice", "__typename": "User" }));
695 }
696
697 #[test]
698 fn test_add_typename_only_array() {
699 let projector = ResultProjector::new(vec!["id".to_string()]);
700
701 let data = json!([
702 { "id": "1", "name": "Alice" },
703 { "id": "2", "name": "Bob" }
704 ]);
705 let jsonb = JsonbValue::new(data);
706 let result = projector.add_typename_only(&jsonb, "User").unwrap();
707
708 assert_eq!(
709 result,
710 json!([
711 { "id": "1", "name": "Alice", "__typename": "User" },
712 { "id": "2", "name": "Bob", "__typename": "User" }
713 ])
714 );
715 }
716
717 #[test]
718 fn test_add_typename_only_primitive() {
719 let projector = ResultProjector::new(vec![]);
720
721 let jsonb = JsonbValue::new(json!("string_value"));
722 let result = projector.add_typename_only(&jsonb, "String").unwrap();
723
724 assert_eq!(result, json!("string_value"));
726 }
727
728 #[test]
733 fn test_field_mapping_simple() {
734 let mapping = FieldMapping::simple("name");
735 assert_eq!(mapping.source, "name");
736 assert_eq!(mapping.output, "name");
737 }
738
739 #[test]
740 fn test_field_mapping_aliased() {
741 let mapping = FieldMapping::aliased("author", "writer");
742 assert_eq!(mapping.source, "author");
743 assert_eq!(mapping.output, "writer");
744 }
745
746 #[test]
747 fn test_project_with_alias() {
748 let mapper = ProjectionMapper::with_mappings(vec![
749 FieldMapping::simple("id"),
750 FieldMapping::aliased("author", "writer"),
751 ]);
752
753 let data = json!({
754 "id": "123",
755 "author": { "name": "Alice" },
756 "title": "Hello World"
757 });
758
759 let jsonb = JsonbValue::new(data);
760 let result = mapper.project(&jsonb).unwrap();
761
762 assert_eq!(
764 result,
765 json!({
766 "id": "123",
767 "writer": { "name": "Alice" }
768 })
769 );
770 }
771
772 #[test]
773 fn test_project_with_typename() {
774 let mapper =
775 ProjectionMapper::new(vec!["id".to_string(), "name".to_string()]).with_typename("User");
776
777 let data = json!({
778 "id": "123",
779 "name": "Alice",
780 "email": "alice@example.com"
781 });
782
783 let jsonb = JsonbValue::new(data);
784 let result = mapper.project(&jsonb).unwrap();
785
786 assert_eq!(
787 result,
788 json!({
789 "__typename": "User",
790 "id": "123",
791 "name": "Alice"
792 })
793 );
794 }
795
796 #[test]
797 fn test_project_with_alias_and_typename() {
798 let mapper = ProjectionMapper::with_mappings(vec![
799 FieldMapping::simple("id"),
800 FieldMapping::aliased("author", "writer"),
801 ])
802 .with_typename("Post");
803
804 let data = json!({
805 "id": "post-1",
806 "author": { "name": "Alice" },
807 "title": "Hello"
808 });
809
810 let jsonb = JsonbValue::new(data);
811 let result = mapper.project(&jsonb).unwrap();
812
813 assert_eq!(
814 result,
815 json!({
816 "__typename": "Post",
817 "id": "post-1",
818 "writer": { "name": "Alice" }
819 })
820 );
821 }
822
823 #[test]
824 fn test_result_projector_with_typename() {
825 let projector =
826 ResultProjector::new(vec!["id".to_string(), "name".to_string()]).with_typename("User");
827
828 let data = json!({ "id": "1", "name": "Alice", "email": "alice@example.com" });
829 let results = vec![JsonbValue::new(data)];
830 let result = projector.project_results(&results, false).unwrap();
831
832 assert_eq!(
833 result,
834 json!({
835 "__typename": "User",
836 "id": "1",
837 "name": "Alice"
838 })
839 );
840 }
841
842 #[test]
843 fn test_result_projector_list_with_typename() {
844 let projector = ResultProjector::new(vec!["id".to_string()]).with_typename("User");
845
846 let results = vec![
847 JsonbValue::new(json!({ "id": "1", "name": "Alice" })),
848 JsonbValue::new(json!({ "id": "2", "name": "Bob" })),
849 ];
850 let result = projector.project_results(&results, true).unwrap();
851
852 assert_eq!(
853 result,
854 json!([
855 { "__typename": "User", "id": "1" },
856 { "__typename": "User", "id": "2" }
857 ])
858 );
859 }
860
861 #[test]
862 fn test_result_projector_with_mappings() {
863 let projector = ResultProjector::with_mappings(vec![
864 FieldMapping::simple("id"),
865 FieldMapping::aliased("full_name", "name"),
866 ]);
867
868 let data = json!({ "id": "1", "full_name": "Alice Smith", "email": "alice@example.com" });
869 let results = vec![JsonbValue::new(data)];
870 let result = projector.project_results(&results, false).unwrap();
871
872 assert_eq!(
874 result,
875 json!({
876 "id": "1",
877 "name": "Alice Smith"
878 })
879 );
880 }
881
882 #[test]
887 fn test_nested_object_typename() {
888 let mapper = ProjectionMapper::with_mappings(vec![
889 FieldMapping::simple("id"),
890 FieldMapping::simple("title"),
891 FieldMapping::nested_object(
892 "author",
893 "User",
894 vec![FieldMapping::simple("id"), FieldMapping::simple("name")],
895 ),
896 ])
897 .with_typename("Post");
898
899 let data = json!({
900 "id": "post-1",
901 "title": "Hello World",
902 "author": {
903 "id": "user-1",
904 "name": "Alice",
905 "email": "alice@example.com"
906 }
907 });
908
909 let jsonb = JsonbValue::new(data);
910 let result = mapper.project(&jsonb).unwrap();
911
912 assert_eq!(
913 result,
914 json!({
915 "__typename": "Post",
916 "id": "post-1",
917 "title": "Hello World",
918 "author": {
919 "__typename": "User",
920 "id": "user-1",
921 "name": "Alice"
922 }
923 })
924 );
925 }
926
927 #[test]
928 fn test_nested_array_typename() {
929 let mapper = ProjectionMapper::with_mappings(vec![
930 FieldMapping::simple("id"),
931 FieldMapping::simple("name"),
932 FieldMapping::nested_object(
933 "posts",
934 "Post",
935 vec![FieldMapping::simple("id"), FieldMapping::simple("title")],
936 ),
937 ])
938 .with_typename("User");
939
940 let data = json!({
941 "id": "user-1",
942 "name": "Alice",
943 "posts": [
944 { "id": "post-1", "title": "First Post", "views": 100 },
945 { "id": "post-2", "title": "Second Post", "views": 200 }
946 ]
947 });
948
949 let jsonb = JsonbValue::new(data);
950 let result = mapper.project(&jsonb).unwrap();
951
952 assert_eq!(
953 result,
954 json!({
955 "__typename": "User",
956 "id": "user-1",
957 "name": "Alice",
958 "posts": [
959 { "__typename": "Post", "id": "post-1", "title": "First Post" },
960 { "__typename": "Post", "id": "post-2", "title": "Second Post" }
961 ]
962 })
963 );
964 }
965
966 #[test]
967 fn test_deeply_nested_typename() {
968 let mapper = ProjectionMapper::with_mappings(vec![
970 FieldMapping::simple("id"),
971 FieldMapping::nested_object(
972 "author",
973 "User",
974 vec![
975 FieldMapping::simple("name"),
976 FieldMapping::nested_object(
977 "company",
978 "Company",
979 vec![FieldMapping::simple("name")],
980 ),
981 ],
982 ),
983 ])
984 .with_typename("Post");
985
986 let data = json!({
987 "id": "post-1",
988 "author": {
989 "name": "Alice",
990 "company": {
991 "name": "Acme Corp",
992 "revenue": 1_000_000
993 }
994 }
995 });
996
997 let jsonb = JsonbValue::new(data);
998 let result = mapper.project(&jsonb).unwrap();
999
1000 assert_eq!(
1001 result,
1002 json!({
1003 "__typename": "Post",
1004 "id": "post-1",
1005 "author": {
1006 "__typename": "User",
1007 "name": "Alice",
1008 "company": {
1009 "__typename": "Company",
1010 "name": "Acme Corp"
1011 }
1012 }
1013 })
1014 );
1015 }
1016
1017 #[test]
1018 fn test_nested_object_with_alias_and_typename() {
1019 let mapper = ProjectionMapper::with_mappings(vec![
1020 FieldMapping::simple("id"),
1021 FieldMapping::nested_object_aliased(
1022 "author",
1023 "writer",
1024 "User",
1025 vec![FieldMapping::simple("id"), FieldMapping::simple("name")],
1026 ),
1027 ])
1028 .with_typename("Post");
1029
1030 let data = json!({
1031 "id": "post-1",
1032 "author": {
1033 "id": "user-1",
1034 "name": "Alice"
1035 }
1036 });
1037
1038 let jsonb = JsonbValue::new(data);
1039 let result = mapper.project(&jsonb).unwrap();
1040
1041 assert_eq!(
1043 result,
1044 json!({
1045 "__typename": "Post",
1046 "id": "post-1",
1047 "writer": {
1048 "__typename": "User",
1049 "id": "user-1",
1050 "name": "Alice"
1051 }
1052 })
1053 );
1054 }
1055
1056 #[test]
1061 fn test_nested_object_as_json_string_is_re_parsed() {
1062 let mapper = ProjectionMapper::with_mappings(vec![
1066 FieldMapping::simple("id"),
1067 FieldMapping::nested_object(
1068 "author",
1069 "User",
1070 vec![FieldMapping::simple("id"), FieldMapping::simple("name")],
1071 ),
1072 ])
1073 .with_typename("Post");
1074
1075 let data = json!({
1077 "id": "post-1",
1078 "author": "{\"id\":\"user-2\",\"name\":\"Bob\"}"
1079 });
1080
1081 let jsonb = JsonbValue::new(data);
1082 let result = mapper.project(&jsonb).unwrap();
1083
1084 let author = result.get("author").expect("author field missing");
1086 assert!(author.is_object(), "author should be a JSON object, got: {:?}", author);
1087 assert_eq!(author.get("id"), Some(&json!("user-2")));
1088 assert_eq!(author.get("name"), Some(&json!("Bob")));
1089 }
1090
1091 fn make_selections_with_typename() -> Vec<FieldSelection> {
1096 vec![FieldSelection {
1097 name: "users".to_string(),
1098 alias: None,
1099 arguments: vec![],
1100 nested_fields: vec![
1101 FieldSelection {
1102 name: "id".to_string(),
1103 alias: None,
1104 arguments: vec![],
1105 nested_fields: vec![],
1106 directives: vec![],
1107 },
1108 FieldSelection {
1109 name: "__typename".to_string(),
1110 alias: None,
1111 arguments: vec![],
1112 nested_fields: vec![],
1113 directives: vec![],
1114 },
1115 ],
1116 directives: vec![],
1117 }]
1118 }
1119
1120 fn make_selections_without_typename() -> Vec<FieldSelection> {
1121 vec![FieldSelection {
1122 name: "users".to_string(),
1123 alias: None,
1124 arguments: vec![],
1125 nested_fields: vec![FieldSelection {
1126 name: "id".to_string(),
1127 alias: None,
1128 arguments: vec![],
1129 nested_fields: vec![],
1130 directives: vec![],
1131 }],
1132 directives: vec![],
1133 }]
1134 }
1135
1136 #[test]
1137 fn test_configure_typename_from_selections_present() {
1138 let projector = ResultProjector::new(vec!["id".to_string()])
1139 .configure_typename_from_selections(&make_selections_with_typename(), "User");
1140
1141 let data = json!({ "id": "1", "name": "Alice" });
1142 let results = vec![JsonbValue::new(data)];
1143 let result = projector.project_results(&results, false).unwrap();
1144
1145 assert_eq!(result, json!({ "__typename": "User", "id": "1" }));
1146 }
1147
1148 #[test]
1149 fn test_configure_typename_from_selections_absent() {
1150 let projector = ResultProjector::new(vec!["id".to_string()])
1151 .configure_typename_from_selections(&make_selections_without_typename(), "User");
1152
1153 let data = json!({ "id": "1", "name": "Alice" });
1154 let results = vec![JsonbValue::new(data)];
1155 let result = projector.project_results(&results, false).unwrap();
1156
1157 assert_eq!(result, json!({ "id": "1" }));
1159 }
1160
1161 #[test]
1162 fn test_configure_typename_from_selections_list() {
1163 let projector = ResultProjector::new(vec!["id".to_string()])
1164 .configure_typename_from_selections(&make_selections_with_typename(), "User");
1165
1166 let results = vec![
1167 JsonbValue::new(json!({ "id": "1" })),
1168 JsonbValue::new(json!({ "id": "2" })),
1169 ];
1170 let result = projector.project_results(&results, true).unwrap();
1171
1172 assert_eq!(
1173 result,
1174 json!([
1175 { "__typename": "User", "id": "1" },
1176 { "__typename": "User", "id": "2" }
1177 ])
1178 );
1179 }
1180
1181 #[test]
1182 fn test_configure_typename_empty_selections() {
1183 let projector = ResultProjector::new(vec!["id".to_string()])
1185 .configure_typename_from_selections(&[], "User");
1186
1187 let data = json!({ "id": "1" });
1188 let results = vec![JsonbValue::new(data)];
1189 let result = projector.project_results(&results, false).unwrap();
1190
1191 assert_eq!(result, json!({ "id": "1" }));
1192 }
1193
1194 #[test]
1199 fn test_federation_mode_injects_typename() {
1200 let projector = ResultProjector::new(vec!["id".to_string()])
1201 .with_typename("User")
1202 .with_federation_mode(true);
1203
1204 let data = json!({ "id": "1", "name": "Alice" });
1205 let results = vec![JsonbValue::new(data)];
1206 let result = projector.project_results(&results, false).unwrap();
1207
1208 assert_eq!(result, json!({ "__typename": "User", "id": "1" }));
1209 }
1210
1211 #[test]
1212 fn test_federation_mode_flag_propagates() {
1213 let mapper = ProjectionMapper::new(vec!["id".to_string()]).with_federation_mode(true);
1214 assert!(mapper.federation_mode);
1215
1216 let mapper2 = ProjectionMapper::new(vec!["id".to_string()]).with_federation_mode(false);
1217 assert!(!mapper2.federation_mode);
1218 }
1219
1220 #[test]
1223 fn test_nested_without_specific_fields() {
1224 let mapper = ProjectionMapper::with_mappings(vec![
1226 FieldMapping::simple("id"),
1227 FieldMapping::simple("author").with_nested_typename("User"),
1228 ])
1229 .with_typename("Post");
1230
1231 let data = json!({
1232 "id": "post-1",
1233 "author": {
1234 "id": "user-1",
1235 "name": "Alice",
1236 "email": "alice@example.com"
1237 }
1238 });
1239
1240 let jsonb = JsonbValue::new(data);
1241 let result = mapper.project(&jsonb).unwrap();
1242
1243 assert_eq!(
1245 result,
1246 json!({
1247 "__typename": "Post",
1248 "id": "post-1",
1249 "author": {
1250 "__typename": "User",
1251 "id": "user-1",
1252 "name": "Alice",
1253 "email": "alice@example.com"
1254 }
1255 })
1256 );
1257 }
1258}