1use adk_core::SchemaAdapter;
55use adk_core::schema_utils;
56use serde_json::{Map, Value};
57use std::borrow::Cow;
58
59const GEMINI_ALLOWED_FORMATS: &[&str] =
61 &["date-time", "date", "time", "email", "uri", "uuid", "int32", "int64", "float", "double"];
62
63const UNSUPPORTED_KEYWORDS: &[&str] = &[
73 "$id",
74 "additionalProperties",
75 "contains",
76 "contentEncoding",
77 "contentMediaType",
78 "default",
79 "dependentRequired",
80 "dependentSchemas",
81 "deprecated",
82 "examples",
83 "exclusiveMaximum",
84 "exclusiveMinimum",
85 "maxItems",
86 "maxLength",
87 "maxProperties",
88 "maximum",
89 "minItems",
90 "minLength",
91 "minProperties",
92 "minimum",
93 "multipleOf",
94 "not",
95 "pattern",
96 "patternProperties",
97 "prefixItems",
98 "propertyNames",
99 "readOnly",
100 "title",
101 "unevaluatedProperties",
102 "uniqueItems",
103 "writeOnly",
104];
105
106const UNSUPPORTED_KEYWORDS_VERTEX: &[&str] = &[
113 "$id",
114 "contains",
115 "contentEncoding",
116 "contentMediaType",
117 "default",
118 "dependentRequired",
119 "dependentSchemas",
120 "deprecated",
121 "examples",
122 "exclusiveMaximum",
123 "exclusiveMinimum",
124 "maxItems",
125 "maxLength",
126 "maxProperties",
127 "maximum",
128 "minItems",
129 "minLength",
130 "minProperties",
131 "minimum",
132 "multipleOf",
133 "not",
134 "pattern",
135 "patternProperties",
136 "prefixItems",
137 "propertyNames",
138 "readOnly",
139 "title",
140 "unevaluatedProperties",
141 "uniqueItems",
142 "writeOnly",
143];
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
177pub enum GeminiSchemaDialect {
178 #[default]
180 OpenApiSubset,
181 VertexOpenApiSubset,
184 JsonSchema,
186}
187
188impl GeminiSchemaDialect {
189 pub fn parameters_field(self) -> &'static str {
191 match self {
192 Self::OpenApiSubset | Self::VertexOpenApiSubset => "parameters",
193 Self::JsonSchema => "parametersJsonSchema",
194 }
195 }
196
197 pub fn requires_openapi_reduction(self) -> bool {
204 match self {
205 Self::OpenApiSubset | Self::VertexOpenApiSubset => true,
206 Self::JsonSchema => false,
207 }
208 }
209}
210
211#[derive(Debug)]
249pub struct GeminiSchemaAdapter {
250 dialect: GeminiSchemaDialect,
252}
253
254impl GeminiSchemaAdapter {
255 pub fn new() -> Self {
259 Self::with_dialect(GeminiSchemaDialect::OpenApiSubset)
260 }
261
262 pub fn vertex_ai() -> Self {
267 Self::with_dialect(GeminiSchemaDialect::VertexOpenApiSubset)
268 }
269
270 pub fn json_schema() -> Self {
278 Self::with_dialect(GeminiSchemaDialect::JsonSchema)
279 }
280
281 pub fn with_dialect(dialect: GeminiSchemaDialect) -> Self {
283 Self { dialect }
284 }
285
286 pub fn dialect(&self) -> GeminiSchemaDialect {
288 self.dialect
289 }
290}
291
292impl Default for GeminiSchemaAdapter {
293 fn default() -> Self {
294 Self::new()
295 }
296}
297
298impl SchemaAdapter for GeminiSchemaAdapter {
299 fn normalize_schema(&self, mut schema: Value) -> Value {
300 let definitions = extract_definitions(&schema);
315 schema_utils::resolve_refs(&mut schema, &definitions, 0);
316
317 schema_utils::strip_schema_keyword(&mut schema);
319
320 if self.dialect.requires_openapi_reduction() {
321 schema_utils::collapse_combiners(&mut schema);
323
324 schema_utils::merge_all_of(&mut schema);
326
327 schema_utils::collapse_type_arrays(&mut schema);
329
330 schema_utils::strip_conditional_keywords(&mut schema);
332
333 schema_utils::convert_const_to_enum(&mut schema);
335
336 schema_utils::strip_null_from_enum(&mut schema);
338
339 schema_utils::add_implicit_object_type(&mut schema);
346 }
347
348 match self.dialect {
354 GeminiSchemaDialect::JsonSchema => {}
359 GeminiSchemaDialect::VertexOpenApiSubset => {
360 remove_unsupported_keywords_vertex(&mut schema)
361 }
362 GeminiSchemaDialect::OpenApiSubset => remove_unsupported_keywords(&mut schema),
363 }
364
365 schema_utils::strip_unsupported_formats(&mut schema, GEMINI_ALLOWED_FORMATS);
367
368 schema_utils::enforce_nesting_depth(&mut schema, 5, 0);
370
371 if let Some(obj) = schema.as_object_mut() {
373 obj.remove("definitions");
374 obj.remove("$defs");
375 }
376
377 schema
378 }
379
380 fn normalize_tool_name<'a>(&self, name: &'a str) -> Cow<'a, str> {
384 if name.len() <= 64 {
385 Cow::Borrowed(name)
386 } else {
387 let mut end = 64;
388 while end > 0 && !name.is_char_boundary(end) {
389 end -= 1;
390 }
391 Cow::Owned(name[..end].to_string())
392 }
393 }
394
395 fn empty_schema(&self) -> Value {
400 serde_json::json!({"type": "object", "properties": {}})
401 }
402
403 fn parameters_field(&self) -> &'static str {
406 self.dialect.parameters_field()
407 }
408}
409
410fn extract_definitions(schema: &Value) -> Map<String, Value> {
413 let mut defs = Map::new();
414
415 if let Some(obj) = schema.as_object() {
416 if let Some(definitions) = obj.get("definitions").and_then(|v| v.as_object()) {
418 for (key, value) in definitions {
419 defs.insert(key.clone(), value.clone());
420 }
421 }
422
423 if let Some(dollar_defs) = obj.get("$defs").and_then(|v| v.as_object()) {
425 for (key, value) in dollar_defs {
426 defs.insert(key.clone(), value.clone());
427 }
428 }
429 }
430
431 defs
432}
433
434fn remove_unsupported_keywords(schema: &mut Value) {
440 let Some(obj) = schema.as_object_mut() else {
441 return;
442 };
443
444 for keyword in UNSUPPORTED_KEYWORDS {
446 obj.remove(*keyword);
447 }
448
449 let is_array_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "array");
457 if !is_array_type {
458 obj.remove("items");
459 } else if obj.get("items").is_some_and(|v| v.is_array()) {
460 let first_schema = obj
462 .get("items")
463 .and_then(|v| v.as_array())
464 .and_then(|arr| arr.first())
465 .cloned()
466 .unwrap_or_else(|| serde_json::json!({"type": "string"}));
467 obj.insert("items".to_string(), first_schema);
468 } else if !obj.contains_key("items") {
469 obj.insert("items".to_string(), serde_json::json!({"type": "string"}));
471 }
472
473 if let Some(props) = obj.get_mut("properties")
475 && let Some(props_obj) = props.as_object_mut()
476 {
477 for value in props_obj.values_mut() {
478 remove_unsupported_keywords(value);
479 }
480 }
481
482 if let Some(items) = obj.get_mut("items")
484 && items.is_object()
485 {
486 remove_unsupported_keywords(items);
487 }
488
489 for keyword in &["allOf", "anyOf", "oneOf"] {
491 if let Some(arr_val) = obj.get_mut(*keyword)
492 && let Some(arr) = arr_val.as_array_mut()
493 {
494 for sub in arr.iter_mut() {
495 remove_unsupported_keywords(sub);
496 }
497 }
498 }
499}
500
501fn remove_unsupported_keywords_vertex(schema: &mut Value) {
508 let Some(obj) = schema.as_object_mut() else {
509 return;
510 };
511
512 for keyword in UNSUPPORTED_KEYWORDS_VERTEX {
514 obj.remove(*keyword);
515 }
516
517 let is_object_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "object");
519 if is_object_type {
520 obj.insert("additionalProperties".to_string(), Value::Bool(false));
521 } else {
522 obj.remove("additionalProperties");
524 }
525
526 let is_array_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "array");
534 if !is_array_type {
535 obj.remove("items");
536 } else if obj.get("items").is_some_and(|v| v.is_array()) {
537 let first_schema = obj
538 .get("items")
539 .and_then(|v| v.as_array())
540 .and_then(|arr| arr.first())
541 .cloned()
542 .unwrap_or_else(|| serde_json::json!({"type": "string"}));
543 obj.insert("items".to_string(), first_schema);
544 } else if !obj.contains_key("items") {
545 obj.insert("items".to_string(), serde_json::json!({"type": "string"}));
546 }
547
548 if let Some(props) = obj.get_mut("properties")
550 && let Some(props_obj) = props.as_object_mut()
551 {
552 for value in props_obj.values_mut() {
553 remove_unsupported_keywords_vertex(value);
554 }
555 }
556
557 if let Some(items) = obj.get_mut("items")
559 && items.is_object()
560 {
561 remove_unsupported_keywords_vertex(items);
562 }
563
564 for keyword in &["allOf", "anyOf", "oneOf"] {
566 if let Some(arr_val) = obj.get_mut(*keyword)
567 && let Some(arr) = arr_val.as_array_mut()
568 {
569 for sub in arr.iter_mut() {
570 remove_unsupported_keywords_vertex(sub);
571 }
572 }
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579 use serde_json::json;
580
581 #[test]
582 fn test_strips_schema_keyword() {
583 let adapter = GeminiSchemaAdapter::new();
584 let schema = json!({
585 "$schema": "http://json-schema.org/draft-07/schema#",
586 "type": "object",
587 "properties": { "name": { "type": "string" } }
588 });
589 let result = adapter.normalize_schema(schema);
590 assert!(result.get("$schema").is_none());
591 }
592
593 #[test]
594 fn test_removes_additional_properties() {
595 let adapter = GeminiSchemaAdapter::new();
596 let schema = json!({
597 "type": "object",
598 "properties": { "name": { "type": "string" } },
599 "additionalProperties": true
600 });
601 let result = adapter.normalize_schema(schema);
602 assert!(result.get("additionalProperties").is_none());
603 }
604
605 #[test]
606 fn test_removes_exclusive_min_max() {
607 let adapter = GeminiSchemaAdapter::new();
608 let schema = json!({
609 "type": "number",
610 "exclusiveMinimum": 0,
611 "exclusiveMaximum": 100
612 });
613 let result = adapter.normalize_schema(schema);
614 assert!(result.get("exclusiveMinimum").is_none());
615 assert!(result.get("exclusiveMaximum").is_none());
616 }
617
618 #[test]
619 fn test_removes_items_when_not_array() {
620 let adapter = GeminiSchemaAdapter::new();
621 let schema = json!({
622 "type": "object",
623 "items": { "type": "string" }
624 });
625 let result = adapter.normalize_schema(schema);
626 assert!(result.get("items").is_none());
627 }
628
629 #[test]
630 fn test_preserves_items_when_array() {
631 let adapter = GeminiSchemaAdapter::new();
632 let schema = json!({
633 "type": "array",
634 "items": { "type": "string" }
635 });
636 let result = adapter.normalize_schema(schema);
637 assert!(result.get("items").is_some());
638 assert_eq!(result["items"]["type"], "string");
639 }
640
641 #[test]
642 fn test_converts_items_tuple_validation_to_single_schema() {
643 let adapter = GeminiSchemaAdapter::new();
646 let schema = json!({
647 "type": "array",
648 "items": [
649 { "type": "number" },
650 { "type": "number" }
651 ]
652 });
653 let result = adapter.normalize_schema(schema);
654 assert_eq!(result["items"], json!({"type": "number"}));
656 assert_eq!(result["type"], "array");
657 }
658
659 #[test]
660 fn test_vertex_ai_converts_items_tuple_validation() {
661 let adapter = GeminiSchemaAdapter::vertex_ai();
662 let schema = json!({
663 "type": "array",
664 "items": [
665 { "type": "integer" },
666 { "type": "boolean" }
667 ]
668 });
669 let result = adapter.normalize_schema(schema);
670 assert_eq!(result["items"], json!({"type": "integer"}));
672 }
673
674 #[test]
675 fn test_removes_not_keyword() {
676 let adapter = GeminiSchemaAdapter::new();
677 let schema = json!({
678 "type": "string",
679 "not": { "enum": ["bad"] }
680 });
681 let result = adapter.normalize_schema(schema);
682 assert!(result.get("not").is_none());
683 }
684
685 #[test]
686 fn test_removes_property_names() {
687 let adapter = GeminiSchemaAdapter::new();
688 let schema = json!({
689 "type": "object",
690 "propertyNames": { "pattern": "^[a-z]+$" }
691 });
692 let result = adapter.normalize_schema(schema);
693 assert!(result.get("propertyNames").is_none());
694 }
695
696 #[test]
697 fn test_removes_pattern_properties() {
698 let adapter = GeminiSchemaAdapter::new();
699 let schema = json!({
700 "type": "object",
701 "patternProperties": { "^S_": { "type": "string" } }
702 });
703 let result = adapter.normalize_schema(schema);
704 assert!(result.get("patternProperties").is_none());
705 }
706
707 #[test]
708 fn test_removes_unevaluated_properties() {
709 let adapter = GeminiSchemaAdapter::new();
710 let schema = json!({
711 "type": "object",
712 "unevaluatedProperties": false
713 });
714 let result = adapter.normalize_schema(schema);
715 assert!(result.get("unevaluatedProperties").is_none());
716 }
717
718 #[test]
719 fn test_collapses_any_of() {
720 let adapter = GeminiSchemaAdapter::new();
721 let schema = json!({
722 "anyOf": [
723 { "type": "null" },
724 { "type": "string", "description": "A non-empty string" }
725 ]
726 });
727 let result = adapter.normalize_schema(schema);
728 assert!(result.get("anyOf").is_none());
729 assert_eq!(result["type"], "string");
730 assert_eq!(result["description"], "A non-empty string");
731 }
732
733 #[test]
734 fn test_collapses_one_of() {
735 let adapter = GeminiSchemaAdapter::new();
736 let schema = json!({
737 "oneOf": [
738 { "type": "null" },
739 { "type": "integer", "minimum": 0 }
740 ]
741 });
742 let result = adapter.normalize_schema(schema);
743 assert!(result.get("oneOf").is_none());
744 assert_eq!(result["type"], "integer");
745 }
746
747 #[test]
748 fn test_merges_all_of() {
749 let adapter = GeminiSchemaAdapter::new();
750 let schema = json!({
751 "allOf": [
752 { "type": "object", "properties": { "a": { "type": "string" } } },
753 { "properties": { "b": { "type": "number" } }, "required": ["b"] }
754 ]
755 });
756 let result = adapter.normalize_schema(schema);
757 assert!(result.get("allOf").is_none());
758 assert_eq!(result["properties"]["a"]["type"], "string");
759 assert_eq!(result["properties"]["b"]["type"], "number");
760 assert_eq!(result["required"], json!(["b"]));
761 }
762
763 #[test]
764 fn test_collapses_type_arrays() {
765 let adapter = GeminiSchemaAdapter::new();
766 let schema = json!({
767 "type": ["string", "null"],
768 "minLength": 1
769 });
770 let result = adapter.normalize_schema(schema);
771 assert_eq!(result["type"], "string");
772 }
773
774 #[test]
775 fn test_strips_conditional_keywords() {
776 let adapter = GeminiSchemaAdapter::new();
777 let schema = json!({
778 "type": "object",
779 "if": { "properties": { "kind": { "const": "a" } } },
780 "then": { "required": ["extra"] },
781 "else": { "required": [] }
782 });
783 let result = adapter.normalize_schema(schema);
784 assert!(result.get("if").is_none());
785 assert!(result.get("then").is_none());
786 assert!(result.get("else").is_none());
787 }
788
789 #[test]
790 fn test_converts_const_to_enum() {
791 let adapter = GeminiSchemaAdapter::new();
792 let schema = json!({
793 "type": "string",
794 "const": "fixed"
795 });
796 let result = adapter.normalize_schema(schema);
797 assert!(result.get("const").is_none());
798 assert_eq!(result["enum"], json!(["fixed"]));
799 }
800
801 #[test]
802 fn test_strips_null_from_enum() {
803 let adapter = GeminiSchemaAdapter::new();
804 let schema = json!({
805 "type": "string",
806 "enum": ["a", null, "b"]
807 });
808 let result = adapter.normalize_schema(schema);
809 assert_eq!(result["enum"], json!(["a", "b"]));
810 }
811
812 #[test]
813 fn test_removes_empty_enum_after_null_strip() {
814 let adapter = GeminiSchemaAdapter::new();
815 let schema = json!({
816 "type": "string",
817 "enum": [null]
818 });
819 let result = adapter.normalize_schema(schema);
820 assert!(result.get("enum").is_none());
821 }
822
823 #[test]
824 fn test_adds_implicit_object_type() {
825 let adapter = GeminiSchemaAdapter::new();
826 let schema = json!({
827 "properties": { "name": { "type": "string" } }
828 });
829 let result = adapter.normalize_schema(schema);
830 assert_eq!(result["type"], "object");
831 }
832
833 #[test]
834 fn test_strips_unsupported_formats() {
835 let adapter = GeminiSchemaAdapter::new();
836 let schema = json!({
837 "type": "object",
838 "properties": {
839 "created": { "type": "string", "format": "date-time" },
840 "hostname": { "type": "string", "format": "hostname" },
841 "id": { "type": "string", "format": "uuid" }
842 }
843 });
844 let result = adapter.normalize_schema(schema);
845 assert_eq!(result["properties"]["created"]["format"], "date-time");
846 assert!(result["properties"]["hostname"].get("format").is_none());
847 assert_eq!(result["properties"]["id"]["format"], "uuid");
848 }
849
850 #[test]
851 fn test_preserves_all_allowed_formats() {
852 let adapter = GeminiSchemaAdapter::new();
853 for format in GEMINI_ALLOWED_FORMATS {
854 let schema = json!({ "type": "string", "format": format });
855 let result = adapter.normalize_schema(schema);
856 assert_eq!(result["format"], *format, "format '{format}' should be preserved");
857 }
858 }
859
860 #[test]
861 fn test_enforces_nesting_depth() {
862 let adapter = GeminiSchemaAdapter::new();
863 let schema = json!({
865 "type": "object",
866 "properties": {
867 "l1": {
868 "type": "object",
869 "properties": {
870 "l2": {
871 "type": "object",
872 "properties": {
873 "l3": {
874 "type": "object",
875 "properties": {
876 "l4": {
877 "type": "object",
878 "properties": {
879 "l5": {
880 "type": "object",
881 "properties": {
882 "l6": { "type": "string" }
883 }
884 }
885 }
886 }
887 }
888 }
889 }
890 }
891 }
892 }
893 }
894 });
895 let result = adapter.normalize_schema(schema);
896 let l5 = &result["properties"]["l1"]["properties"]["l2"]["properties"]["l3"]["properties"]
898 ["l4"]["properties"]["l5"];
899 assert_eq!(l5, &json!({"type": "object"}));
900 }
901
902 #[test]
903 fn test_resolves_refs() {
904 let adapter = GeminiSchemaAdapter::new();
905 let schema = json!({
906 "type": "object",
907 "properties": {
908 "address": { "$ref": "#/definitions/Address" }
909 },
910 "definitions": {
911 "Address": {
912 "type": "object",
913 "properties": {
914 "street": { "type": "string" }
915 }
916 }
917 }
918 });
919 let result = adapter.normalize_schema(schema);
920 assert!(result["properties"]["address"].get("$ref").is_none());
922 assert_eq!(result["properties"]["address"]["type"], "object");
923 assert_eq!(result["properties"]["address"]["properties"]["street"]["type"], "string");
924 assert!(result.get("definitions").is_none());
926 }
927
928 #[test]
929 fn test_resolves_dollar_defs() {
930 let adapter = GeminiSchemaAdapter::new();
931 let schema = json!({
932 "type": "object",
933 "properties": {
934 "item": { "$ref": "#/$defs/Item" }
935 },
936 "$defs": {
937 "Item": {
938 "type": "object",
939 "properties": {
940 "name": { "type": "string" }
941 }
942 }
943 }
944 });
945 let result = adapter.normalize_schema(schema);
946 assert!(result["properties"]["item"].get("$ref").is_none());
947 assert_eq!(result["properties"]["item"]["type"], "object");
948 assert!(result.get("$defs").is_none());
949 }
950
951 #[test]
952 fn test_unresolvable_ref_becomes_object() {
953 let adapter = GeminiSchemaAdapter::new();
954 let schema = json!({
955 "type": "object",
956 "properties": {
957 "unknown": { "$ref": "#/definitions/DoesNotExist" }
958 }
959 });
960 let result = adapter.normalize_schema(schema);
961 assert_eq!(result["properties"]["unknown"], json!({"type": "object"}));
962 }
963
964 #[test]
965 fn test_circular_ref_breaks() {
966 let adapter = GeminiSchemaAdapter::new();
967 let schema = json!({
968 "type": "object",
969 "properties": {
970 "self_ref": { "$ref": "#/definitions/Node" }
971 },
972 "definitions": {
973 "Node": {
974 "type": "object",
975 "properties": {
976 "child": { "$ref": "#/definitions/Node" }
977 }
978 }
979 }
980 });
981 let result = adapter.normalize_schema(schema);
982 assert_eq!(result["properties"]["self_ref"]["type"], "object");
984 assert!(result.get("definitions").is_none());
985 }
986
987 #[test]
988 fn test_removes_definitions_and_defs() {
989 let adapter = GeminiSchemaAdapter::new();
990 let schema = json!({
991 "type": "object",
992 "definitions": { "Foo": { "type": "string" } },
993 "$defs": { "Bar": { "type": "number" } }
994 });
995 let result = adapter.normalize_schema(schema);
996 assert!(result.get("definitions").is_none());
997 assert!(result.get("$defs").is_none());
998 }
999
1000 #[test]
1001 fn test_nested_unsupported_keywords_removed() {
1002 let adapter = GeminiSchemaAdapter::new();
1003 let schema = json!({
1004 "type": "object",
1005 "properties": {
1006 "inner": {
1007 "type": "object",
1008 "additionalProperties": false,
1009 "exclusiveMinimum": 5,
1010 "properties": {
1011 "deep": {
1012 "type": "number",
1013 "exclusiveMaximum": 100
1014 }
1015 }
1016 }
1017 }
1018 });
1019 let result = adapter.normalize_schema(schema);
1020 let inner = &result["properties"]["inner"];
1021 assert!(inner.get("additionalProperties").is_none());
1022 assert!(inner.get("exclusiveMinimum").is_none());
1023 assert!(inner["properties"]["deep"].get("exclusiveMaximum").is_none());
1024 }
1025
1026 #[test]
1027 fn test_full_transform_pipeline() {
1028 let adapter = GeminiSchemaAdapter::new();
1029 let schema = json!({
1030 "$schema": "http://json-schema.org/draft-07/schema#",
1031 "definitions": {
1032 "Status": { "type": "string", "enum": ["active", null, "inactive"] }
1033 },
1034 "properties": {
1035 "name": { "type": ["string", "null"], "format": "hostname" },
1036 "status": { "$ref": "#/definitions/Status" },
1037 "config": {
1038 "type": "object",
1039 "additionalProperties": true,
1040 "properties": {
1041 "value": { "const": "fixed" }
1042 }
1043 }
1044 },
1045 "if": { "properties": { "name": { "type": "string" } } },
1046 "then": { "required": ["status"] },
1047 "additionalProperties": false
1048 });
1049 let result = adapter.normalize_schema(schema);
1050
1051 assert!(result.get("$schema").is_none());
1053 assert!(result.get("definitions").is_none());
1055 assert!(result.get("if").is_none());
1057 assert!(result.get("then").is_none());
1058 assert!(result.get("additionalProperties").is_none());
1060 assert_eq!(result["properties"]["name"]["type"], "string");
1062 assert!(result["properties"]["name"].get("format").is_none());
1064 assert_eq!(result["properties"]["status"]["enum"], json!(["active", "inactive"]));
1066 assert_eq!(result["properties"]["config"]["properties"]["value"]["enum"], json!(["fixed"]));
1068 assert!(result["properties"]["config"].get("additionalProperties").is_none());
1070 assert_eq!(result["type"], "object");
1072 }
1073
1074 #[test]
1075 fn test_idempotent() {
1076 let adapter = GeminiSchemaAdapter::new();
1077 let schema = json!({
1078 "$schema": "http://json-schema.org/draft-07/schema#",
1079 "type": "object",
1080 "properties": {
1081 "name": { "type": ["string", "null"], "format": "hostname" },
1082 "items": { "type": "array", "items": { "type": "string" } }
1083 },
1084 "additionalProperties": true,
1085 "if": { "const": true },
1086 "then": { "required": ["name"] }
1087 });
1088 let first = adapter.normalize_schema(schema);
1089 let second = adapter.normalize_schema(first.clone());
1090 assert_eq!(first, second);
1091 }
1092
1093 #[test]
1094 fn test_empty_schema() {
1095 let adapter = GeminiSchemaAdapter::new();
1096 let schema = json!({});
1097 let result = adapter.normalize_schema(schema);
1098 assert_eq!(result, json!({}));
1099 }
1100
1101 #[test]
1102 fn test_array_items_nested_cleanup() {
1103 let adapter = GeminiSchemaAdapter::new();
1104 let schema = json!({
1105 "type": "array",
1106 "items": {
1107 "type": "object",
1108 "additionalProperties": true,
1109 "properties": {
1110 "id": { "type": "integer", "exclusiveMinimum": 0 }
1111 }
1112 }
1113 });
1114 let result = adapter.normalize_schema(schema);
1115 assert!(result["items"].get("additionalProperties").is_none());
1116 assert!(result["items"]["properties"]["id"].get("exclusiveMinimum").is_none());
1117 }
1118
1119 #[test]
1122 fn test_vertex_ai_sets_additional_properties_false() {
1123 let adapter = GeminiSchemaAdapter::vertex_ai();
1124 let schema = json!({
1125 "type": "object",
1126 "properties": { "name": { "type": "string" } },
1127 "additionalProperties": true
1128 });
1129 let result = adapter.normalize_schema(schema);
1130 assert_eq!(result["additionalProperties"], json!(false));
1131 }
1132
1133 #[test]
1134 fn test_vertex_ai_sets_additional_properties_false_on_nested_objects() {
1135 let adapter = GeminiSchemaAdapter::vertex_ai();
1136 let schema = json!({
1137 "type": "object",
1138 "properties": {
1139 "inner": {
1140 "type": "object",
1141 "properties": {
1142 "value": { "type": "string" }
1143 }
1144 }
1145 }
1146 });
1147 let result = adapter.normalize_schema(schema);
1148 assert_eq!(result["additionalProperties"], json!(false));
1149 assert_eq!(result["properties"]["inner"]["additionalProperties"], json!(false));
1150 }
1151
1152 #[test]
1153 fn test_vertex_ai_does_not_set_additional_properties_on_non_object() {
1154 let adapter = GeminiSchemaAdapter::vertex_ai();
1155 let schema = json!({
1156 "type": "string",
1157 "additionalProperties": true
1158 });
1159 let result = adapter.normalize_schema(schema);
1160 assert!(result.get("additionalProperties").is_none());
1162 }
1163
1164 #[test]
1165 fn test_standard_mode_removes_additional_properties() {
1166 let adapter = GeminiSchemaAdapter::new();
1167 let schema = json!({
1168 "type": "object",
1169 "properties": { "name": { "type": "string" } },
1170 "additionalProperties": true
1171 });
1172 let result = adapter.normalize_schema(schema);
1173 assert!(result.get("additionalProperties").is_none());
1174 }
1175
1176 #[test]
1177 fn test_vertex_ai_still_removes_other_unsupported_keywords() {
1178 let adapter = GeminiSchemaAdapter::vertex_ai();
1179 let schema = json!({
1180 "type": "object",
1181 "properties": { "x": { "type": "number" } },
1182 "exclusiveMinimum": 0,
1183 "exclusiveMaximum": 100,
1184 "not": { "type": "null" },
1185 "propertyNames": { "pattern": "^[a-z]" },
1186 "patternProperties": { "^S_": { "type": "string" } },
1187 "unevaluatedProperties": false
1188 });
1189 let result = adapter.normalize_schema(schema);
1190 assert!(result.get("exclusiveMinimum").is_none());
1191 assert!(result.get("exclusiveMaximum").is_none());
1192 assert!(result.get("not").is_none());
1193 assert!(result.get("propertyNames").is_none());
1194 assert!(result.get("patternProperties").is_none());
1195 assert!(result.get("unevaluatedProperties").is_none());
1196 assert_eq!(result["additionalProperties"], json!(false));
1198 }
1199
1200 #[test]
1203 fn test_normalize_tool_name_short_name_unchanged() {
1204 let adapter = GeminiSchemaAdapter::new();
1205 let name = "get_weather";
1206 let result = adapter.normalize_tool_name(name);
1207 assert_eq!(result, "get_weather");
1208 assert!(matches!(result, Cow::Borrowed(_)));
1209 }
1210
1211 #[test]
1212 fn test_normalize_tool_name_exactly_64_bytes() {
1213 let adapter = GeminiSchemaAdapter::new();
1214 let name = "a".repeat(64);
1215 let result = adapter.normalize_tool_name(&name);
1216 assert_eq!(result.len(), 64);
1217 assert!(matches!(result, Cow::Borrowed(_)));
1218 }
1219
1220 #[test]
1221 fn test_normalize_tool_name_truncates_at_64_bytes() {
1222 let adapter = GeminiSchemaAdapter::new();
1223 let name = "a".repeat(100);
1224 let result = adapter.normalize_tool_name(&name);
1225 assert_eq!(result.len(), 64);
1226 assert_eq!(result.as_ref(), "a".repeat(64));
1227 }
1228
1229 #[test]
1230 fn test_normalize_tool_name_multibyte_boundary() {
1231 let adapter = GeminiSchemaAdapter::new();
1232 let name = "日".repeat(22); let result = adapter.normalize_tool_name(&name);
1236 assert!(result.len() <= 64);
1237 assert_eq!(result.len(), 63);
1239 assert_eq!(result.as_ref(), "日".repeat(21));
1240 assert!(std::str::from_utf8(result.as_bytes()).is_ok());
1242 }
1243
1244 #[test]
1245 fn test_normalize_tool_name_emoji_boundary() {
1246 let adapter = GeminiSchemaAdapter::new();
1247 let name = "🎯".repeat(16);
1249 assert_eq!(name.len(), 64);
1250 let result = adapter.normalize_tool_name(&name);
1251 assert_eq!(result.len(), 64);
1252
1253 let name = "🎯".repeat(17);
1255 let result = adapter.normalize_tool_name(&name);
1256 assert_eq!(result.len(), 64);
1257 assert_eq!(result.as_ref(), "🎯".repeat(16));
1258 }
1259
1260 #[test]
1263 fn test_empty_schema_returns_object_with_properties() {
1264 let adapter = GeminiSchemaAdapter::new();
1265 let result = adapter.empty_schema();
1266 assert_eq!(result, json!({"type": "object", "properties": {}}));
1267 }
1268
1269 #[test]
1270 fn test_empty_schema_vertex_ai_same_as_standard() {
1271 let adapter = GeminiSchemaAdapter::vertex_ai();
1272 let result = adapter.empty_schema();
1273 assert_eq!(result, json!({"type": "object", "properties": {}}));
1274 }
1275
1276 #[test]
1280 fn test_removes_all_validation_keywords() {
1281 let adapter = GeminiSchemaAdapter::new();
1282 let schema = json!({
1283 "type": "object",
1284 "title": "MySchema",
1285 "$id": "https://example.com/schema",
1286 "default": {},
1287 "deprecated": true,
1288 "readOnly": true,
1289 "writeOnly": false,
1290 "examples": [{"name": "test"}],
1291 "minProperties": 1,
1292 "maxProperties": 10,
1293 "properties": {
1294 "name": {
1295 "type": "string",
1296 "title": "Name",
1297 "default": "",
1298 "minLength": 1,
1299 "maxLength": 100,
1300 "pattern": "^[a-z]+$"
1301 },
1302 "age": {
1303 "type": "integer",
1304 "minimum": 0,
1305 "maximum": 150,
1306 "multipleOf": 1
1307 },
1308 "tags": {
1309 "type": "array",
1310 "items": { "type": "string" },
1311 "minItems": 1,
1312 "maxItems": 10,
1313 "uniqueItems": true,
1314 "contains": { "type": "string" }
1315 }
1316 }
1317 });
1318 let result = adapter.normalize_schema(schema);
1319
1320 assert!(result.get("title").is_none());
1322 assert!(result.get("$id").is_none());
1323 assert!(result.get("default").is_none());
1324 assert!(result.get("deprecated").is_none());
1325 assert!(result.get("readOnly").is_none());
1326 assert!(result.get("writeOnly").is_none());
1327 assert!(result.get("examples").is_none());
1328 assert!(result.get("minProperties").is_none());
1329 assert!(result.get("maxProperties").is_none());
1330
1331 let name = &result["properties"]["name"];
1333 assert!(name.get("title").is_none());
1334 assert!(name.get("default").is_none());
1335 assert!(name.get("minLength").is_none());
1336 assert!(name.get("maxLength").is_none());
1337 assert!(name.get("pattern").is_none());
1338 assert_eq!(name["type"], "string");
1339
1340 let age = &result["properties"]["age"];
1342 assert!(age.get("minimum").is_none());
1343 assert!(age.get("maximum").is_none());
1344 assert!(age.get("multipleOf").is_none());
1345 assert_eq!(age["type"], "integer");
1346
1347 let tags = &result["properties"]["tags"];
1349 assert!(tags.get("minItems").is_none());
1350 assert!(tags.get("maxItems").is_none());
1351 assert!(tags.get("uniqueItems").is_none());
1352 assert!(tags.get("contains").is_none());
1353 assert_eq!(tags["type"], "array");
1354 assert_eq!(tags["items"]["type"], "string");
1355 }
1356
1357 #[test]
1358 fn test_removes_prefix_items() {
1359 let adapter = GeminiSchemaAdapter::new();
1360 let schema = json!({
1361 "type": "array",
1362 "prefixItems": [
1363 { "type": "string" },
1364 { "type": "integer" }
1365 ]
1366 });
1367 let result = adapter.normalize_schema(schema);
1368 assert!(result.get("prefixItems").is_none());
1369 }
1370
1371 #[test]
1372 fn test_removes_dependent_keywords() {
1373 let adapter = GeminiSchemaAdapter::new();
1374 let schema = json!({
1375 "type": "object",
1376 "properties": {
1377 "name": { "type": "string" },
1378 "credit_card": { "type": "string" }
1379 },
1380 "dependentRequired": {
1381 "credit_card": ["billing_address"]
1382 },
1383 "dependentSchemas": {
1384 "credit_card": {
1385 "properties": {
1386 "billing_address": { "type": "string" }
1387 }
1388 }
1389 }
1390 });
1391 let result = adapter.normalize_schema(schema);
1392 assert!(result.get("dependentRequired").is_none());
1393 assert!(result.get("dependentSchemas").is_none());
1394 }
1395
1396 #[test]
1397 fn test_removes_content_keywords() {
1398 let adapter = GeminiSchemaAdapter::new();
1399 let schema = json!({
1400 "type": "string",
1401 "contentMediaType": "application/json",
1402 "contentEncoding": "base64"
1403 });
1404 let result = adapter.normalize_schema(schema);
1405 assert!(result.get("contentMediaType").is_none());
1406 assert!(result.get("contentEncoding").is_none());
1407 }
1408
1409 #[test]
1413 fn parameters_field_follows_the_dialect() {
1414 assert_eq!(GeminiSchemaAdapter::new().parameters_field(), "parameters");
1415 assert_eq!(GeminiSchemaAdapter::vertex_ai().parameters_field(), "parameters");
1416 assert_eq!(GeminiSchemaAdapter::json_schema().parameters_field(), "parametersJsonSchema");
1417 assert_eq!(GeminiSchemaDialect::default(), GeminiSchemaDialect::OpenApiSubset);
1420 }
1421
1422 fn schema_with_constraints_the_subset_cannot_carry() -> Value {
1427 json!({
1428 "type": "object",
1429 "additionalProperties": false,
1430 "properties": {
1431 "request_kind": {"type": "string", "enum": ["order", "information"]},
1432 "callback_number": {"type": "string", "minLength": 7},
1433 "party_size": {"type": "integer", "minimum": 1, "maximum": 40}
1434 },
1435 "required": ["request_kind"],
1436 "allOf": [{
1437 "if": {"properties": {"request_kind": {"const": "order"}}, "required": ["request_kind"]},
1438 "then": {"required": ["callback_number"]}
1439 }]
1440 })
1441 }
1442
1443 #[test]
1444 fn json_schema_dialect_keeps_what_the_subset_strips() {
1445 let result = GeminiSchemaAdapter::json_schema()
1446 .normalize_schema(schema_with_constraints_the_subset_cannot_carry());
1447
1448 assert_eq!(result["additionalProperties"], json!(false));
1449 assert!(result.get("allOf").is_some(), "conditional rule dropped: {result}");
1450 assert_eq!(result["properties"]["callback_number"]["minLength"], 7);
1451 assert_eq!(result["properties"]["party_size"]["minimum"], 1);
1452 assert_eq!(result["properties"]["party_size"]["maximum"], 40);
1453 }
1454
1455 #[test]
1460 fn json_schema_dialect_does_not_stamp_a_type_onto_conditionals() {
1461 let result = GeminiSchemaAdapter::json_schema()
1462 .normalize_schema(schema_with_constraints_the_subset_cannot_carry());
1463
1464 let if_clause = &result["allOf"][0]["if"];
1465 assert!(
1466 if_clause.get("type").is_none(),
1467 "an implicit object type was injected into the `if` clause: {if_clause}"
1468 );
1469 }
1470
1471 #[test]
1474 fn openapi_subset_dialects_still_reduce_as_before() {
1475 for adapter in [GeminiSchemaAdapter::new(), GeminiSchemaAdapter::vertex_ai()] {
1476 assert_eq!(adapter.parameters_field(), "parameters");
1479
1480 let result =
1481 adapter.normalize_schema(schema_with_constraints_the_subset_cannot_carry());
1482
1483 assert!(result.get("allOf").is_none(), "{result}");
1484 assert!(result["properties"]["callback_number"].get("minLength").is_none());
1485 assert!(result["properties"]["party_size"].get("minimum").is_none());
1486 }
1487 }
1488}