1use adk_core::SchemaAdapter;
48use adk_core::schema_utils;
49use serde_json::{Map, Value};
50use std::borrow::Cow;
51
52const GEMINI_ALLOWED_FORMATS: &[&str] =
54 &["date-time", "date", "time", "email", "uri", "uuid", "int32", "int64", "float", "double"];
55
56const UNSUPPORTED_KEYWORDS: &[&str] = &[
66 "$id",
67 "additionalProperties",
68 "contains",
69 "contentEncoding",
70 "contentMediaType",
71 "default",
72 "dependentRequired",
73 "dependentSchemas",
74 "deprecated",
75 "examples",
76 "exclusiveMaximum",
77 "exclusiveMinimum",
78 "maxItems",
79 "maxLength",
80 "maxProperties",
81 "maximum",
82 "minItems",
83 "minLength",
84 "minProperties",
85 "minimum",
86 "multipleOf",
87 "not",
88 "pattern",
89 "patternProperties",
90 "prefixItems",
91 "propertyNames",
92 "readOnly",
93 "title",
94 "unevaluatedProperties",
95 "uniqueItems",
96 "writeOnly",
97];
98
99const UNSUPPORTED_KEYWORDS_VERTEX: &[&str] = &[
106 "$id",
107 "contains",
108 "contentEncoding",
109 "contentMediaType",
110 "default",
111 "dependentRequired",
112 "dependentSchemas",
113 "deprecated",
114 "examples",
115 "exclusiveMaximum",
116 "exclusiveMinimum",
117 "maxItems",
118 "maxLength",
119 "maxProperties",
120 "maximum",
121 "minItems",
122 "minLength",
123 "minProperties",
124 "minimum",
125 "multipleOf",
126 "not",
127 "pattern",
128 "patternProperties",
129 "prefixItems",
130 "propertyNames",
131 "readOnly",
132 "title",
133 "unevaluatedProperties",
134 "uniqueItems",
135 "writeOnly",
136];
137
138#[derive(Debug)]
167pub struct GeminiSchemaAdapter {
168 vertex_ai: bool,
171}
172
173impl GeminiSchemaAdapter {
174 pub fn new() -> Self {
178 Self { vertex_ai: false }
179 }
180
181 pub fn vertex_ai() -> Self {
186 Self { vertex_ai: true }
187 }
188}
189
190impl Default for GeminiSchemaAdapter {
191 fn default() -> Self {
192 Self::new()
193 }
194}
195
196impl SchemaAdapter for GeminiSchemaAdapter {
197 fn normalize_schema(&self, mut schema: Value) -> Value {
198 let definitions = extract_definitions(&schema);
202 schema_utils::resolve_refs(&mut schema, &definitions, 0);
203
204 schema_utils::strip_schema_keyword(&mut schema);
206
207 schema_utils::collapse_combiners(&mut schema);
209
210 schema_utils::merge_all_of(&mut schema);
212
213 schema_utils::collapse_type_arrays(&mut schema);
215
216 schema_utils::strip_conditional_keywords(&mut schema);
218
219 schema_utils::convert_const_to_enum(&mut schema);
221
222 schema_utils::strip_null_from_enum(&mut schema);
224
225 schema_utils::add_implicit_object_type(&mut schema);
227
228 if self.vertex_ai {
230 remove_unsupported_keywords_vertex(&mut schema);
231 } else {
232 remove_unsupported_keywords(&mut schema);
233 }
234
235 schema_utils::strip_unsupported_formats(&mut schema, GEMINI_ALLOWED_FORMATS);
237
238 schema_utils::enforce_nesting_depth(&mut schema, 5, 0);
240
241 if let Some(obj) = schema.as_object_mut() {
243 obj.remove("definitions");
244 obj.remove("$defs");
245 }
246
247 schema
248 }
249
250 fn normalize_tool_name<'a>(&self, name: &'a str) -> Cow<'a, str> {
254 if name.len() <= 64 {
255 Cow::Borrowed(name)
256 } else {
257 let mut end = 64;
258 while end > 0 && !name.is_char_boundary(end) {
259 end -= 1;
260 }
261 Cow::Owned(name[..end].to_string())
262 }
263 }
264
265 fn empty_schema(&self) -> Value {
270 serde_json::json!({"type": "object", "properties": {}})
271 }
272}
273
274fn extract_definitions(schema: &Value) -> Map<String, Value> {
277 let mut defs = Map::new();
278
279 if let Some(obj) = schema.as_object() {
280 if let Some(definitions) = obj.get("definitions").and_then(|v| v.as_object()) {
282 for (key, value) in definitions {
283 defs.insert(key.clone(), value.clone());
284 }
285 }
286
287 if let Some(dollar_defs) = obj.get("$defs").and_then(|v| v.as_object()) {
289 for (key, value) in dollar_defs {
290 defs.insert(key.clone(), value.clone());
291 }
292 }
293 }
294
295 defs
296}
297
298fn remove_unsupported_keywords(schema: &mut Value) {
304 let Some(obj) = schema.as_object_mut() else {
305 return;
306 };
307
308 for keyword in UNSUPPORTED_KEYWORDS {
310 obj.remove(*keyword);
311 }
312
313 let is_array_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "array");
318 let items_is_tuple = obj.get("items").is_some_and(|v| v.is_array());
319 if !is_array_type || items_is_tuple {
320 obj.remove("items");
321 }
322
323 if let Some(props) = obj.get_mut("properties")
325 && let Some(props_obj) = props.as_object_mut()
326 {
327 for value in props_obj.values_mut() {
328 remove_unsupported_keywords(value);
329 }
330 }
331
332 if let Some(items) = obj.get_mut("items")
334 && items.is_object()
335 {
336 remove_unsupported_keywords(items);
337 }
338
339 for keyword in &["allOf", "anyOf", "oneOf"] {
341 if let Some(arr_val) = obj.get_mut(*keyword)
342 && let Some(arr) = arr_val.as_array_mut()
343 {
344 for sub in arr.iter_mut() {
345 remove_unsupported_keywords(sub);
346 }
347 }
348 }
349}
350
351fn remove_unsupported_keywords_vertex(schema: &mut Value) {
358 let Some(obj) = schema.as_object_mut() else {
359 return;
360 };
361
362 for keyword in UNSUPPORTED_KEYWORDS_VERTEX {
364 obj.remove(*keyword);
365 }
366
367 let is_object_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "object");
369 if is_object_type {
370 obj.insert("additionalProperties".to_string(), Value::Bool(false));
371 } else {
372 obj.remove("additionalProperties");
374 }
375
376 let is_array_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "array");
381 let items_is_tuple = obj.get("items").is_some_and(|v| v.is_array());
382 if !is_array_type || items_is_tuple {
383 obj.remove("items");
384 }
385
386 if let Some(props) = obj.get_mut("properties")
388 && let Some(props_obj) = props.as_object_mut()
389 {
390 for value in props_obj.values_mut() {
391 remove_unsupported_keywords_vertex(value);
392 }
393 }
394
395 if let Some(items) = obj.get_mut("items")
397 && items.is_object()
398 {
399 remove_unsupported_keywords_vertex(items);
400 }
401
402 for keyword in &["allOf", "anyOf", "oneOf"] {
404 if let Some(arr_val) = obj.get_mut(*keyword)
405 && let Some(arr) = arr_val.as_array_mut()
406 {
407 for sub in arr.iter_mut() {
408 remove_unsupported_keywords_vertex(sub);
409 }
410 }
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use serde_json::json;
418
419 #[test]
420 fn test_strips_schema_keyword() {
421 let adapter = GeminiSchemaAdapter::new();
422 let schema = json!({
423 "$schema": "http://json-schema.org/draft-07/schema#",
424 "type": "object",
425 "properties": { "name": { "type": "string" } }
426 });
427 let result = adapter.normalize_schema(schema);
428 assert!(result.get("$schema").is_none());
429 }
430
431 #[test]
432 fn test_removes_additional_properties() {
433 let adapter = GeminiSchemaAdapter::new();
434 let schema = json!({
435 "type": "object",
436 "properties": { "name": { "type": "string" } },
437 "additionalProperties": true
438 });
439 let result = adapter.normalize_schema(schema);
440 assert!(result.get("additionalProperties").is_none());
441 }
442
443 #[test]
444 fn test_removes_exclusive_min_max() {
445 let adapter = GeminiSchemaAdapter::new();
446 let schema = json!({
447 "type": "number",
448 "exclusiveMinimum": 0,
449 "exclusiveMaximum": 100
450 });
451 let result = adapter.normalize_schema(schema);
452 assert!(result.get("exclusiveMinimum").is_none());
453 assert!(result.get("exclusiveMaximum").is_none());
454 }
455
456 #[test]
457 fn test_removes_items_when_not_array() {
458 let adapter = GeminiSchemaAdapter::new();
459 let schema = json!({
460 "type": "object",
461 "items": { "type": "string" }
462 });
463 let result = adapter.normalize_schema(schema);
464 assert!(result.get("items").is_none());
465 }
466
467 #[test]
468 fn test_preserves_items_when_array() {
469 let adapter = GeminiSchemaAdapter::new();
470 let schema = json!({
471 "type": "array",
472 "items": { "type": "string" }
473 });
474 let result = adapter.normalize_schema(schema);
475 assert!(result.get("items").is_some());
476 assert_eq!(result["items"]["type"], "string");
477 }
478
479 #[test]
480 fn test_removes_items_tuple_validation_on_array() {
481 let adapter = GeminiSchemaAdapter::new();
484 let schema = json!({
485 "type": "array",
486 "items": [
487 { "type": "string" },
488 { "type": "number" }
489 ]
490 });
491 let result = adapter.normalize_schema(schema);
492 assert!(result.get("items").is_none(), "tuple validation items should be stripped");
493 assert_eq!(result["type"], "array");
494 }
495
496 #[test]
497 fn test_vertex_ai_removes_items_tuple_validation() {
498 let adapter = GeminiSchemaAdapter::vertex_ai();
499 let schema = json!({
500 "type": "array",
501 "items": [
502 { "type": "integer" },
503 { "type": "boolean" }
504 ]
505 });
506 let result = adapter.normalize_schema(schema);
507 assert!(
508 result.get("items").is_none(),
509 "tuple validation items should be stripped on Vertex AI"
510 );
511 }
512
513 #[test]
514 fn test_removes_not_keyword() {
515 let adapter = GeminiSchemaAdapter::new();
516 let schema = json!({
517 "type": "string",
518 "not": { "enum": ["bad"] }
519 });
520 let result = adapter.normalize_schema(schema);
521 assert!(result.get("not").is_none());
522 }
523
524 #[test]
525 fn test_removes_property_names() {
526 let adapter = GeminiSchemaAdapter::new();
527 let schema = json!({
528 "type": "object",
529 "propertyNames": { "pattern": "^[a-z]+$" }
530 });
531 let result = adapter.normalize_schema(schema);
532 assert!(result.get("propertyNames").is_none());
533 }
534
535 #[test]
536 fn test_removes_pattern_properties() {
537 let adapter = GeminiSchemaAdapter::new();
538 let schema = json!({
539 "type": "object",
540 "patternProperties": { "^S_": { "type": "string" } }
541 });
542 let result = adapter.normalize_schema(schema);
543 assert!(result.get("patternProperties").is_none());
544 }
545
546 #[test]
547 fn test_removes_unevaluated_properties() {
548 let adapter = GeminiSchemaAdapter::new();
549 let schema = json!({
550 "type": "object",
551 "unevaluatedProperties": false
552 });
553 let result = adapter.normalize_schema(schema);
554 assert!(result.get("unevaluatedProperties").is_none());
555 }
556
557 #[test]
558 fn test_collapses_any_of() {
559 let adapter = GeminiSchemaAdapter::new();
560 let schema = json!({
561 "anyOf": [
562 { "type": "null" },
563 { "type": "string", "description": "A non-empty string" }
564 ]
565 });
566 let result = adapter.normalize_schema(schema);
567 assert!(result.get("anyOf").is_none());
568 assert_eq!(result["type"], "string");
569 assert_eq!(result["description"], "A non-empty string");
570 }
571
572 #[test]
573 fn test_collapses_one_of() {
574 let adapter = GeminiSchemaAdapter::new();
575 let schema = json!({
576 "oneOf": [
577 { "type": "null" },
578 { "type": "integer", "minimum": 0 }
579 ]
580 });
581 let result = adapter.normalize_schema(schema);
582 assert!(result.get("oneOf").is_none());
583 assert_eq!(result["type"], "integer");
584 }
585
586 #[test]
587 fn test_merges_all_of() {
588 let adapter = GeminiSchemaAdapter::new();
589 let schema = json!({
590 "allOf": [
591 { "type": "object", "properties": { "a": { "type": "string" } } },
592 { "properties": { "b": { "type": "number" } }, "required": ["b"] }
593 ]
594 });
595 let result = adapter.normalize_schema(schema);
596 assert!(result.get("allOf").is_none());
597 assert_eq!(result["properties"]["a"]["type"], "string");
598 assert_eq!(result["properties"]["b"]["type"], "number");
599 assert_eq!(result["required"], json!(["b"]));
600 }
601
602 #[test]
603 fn test_collapses_type_arrays() {
604 let adapter = GeminiSchemaAdapter::new();
605 let schema = json!({
606 "type": ["string", "null"],
607 "minLength": 1
608 });
609 let result = adapter.normalize_schema(schema);
610 assert_eq!(result["type"], "string");
611 }
612
613 #[test]
614 fn test_strips_conditional_keywords() {
615 let adapter = GeminiSchemaAdapter::new();
616 let schema = json!({
617 "type": "object",
618 "if": { "properties": { "kind": { "const": "a" } } },
619 "then": { "required": ["extra"] },
620 "else": { "required": [] }
621 });
622 let result = adapter.normalize_schema(schema);
623 assert!(result.get("if").is_none());
624 assert!(result.get("then").is_none());
625 assert!(result.get("else").is_none());
626 }
627
628 #[test]
629 fn test_converts_const_to_enum() {
630 let adapter = GeminiSchemaAdapter::new();
631 let schema = json!({
632 "type": "string",
633 "const": "fixed"
634 });
635 let result = adapter.normalize_schema(schema);
636 assert!(result.get("const").is_none());
637 assert_eq!(result["enum"], json!(["fixed"]));
638 }
639
640 #[test]
641 fn test_strips_null_from_enum() {
642 let adapter = GeminiSchemaAdapter::new();
643 let schema = json!({
644 "type": "string",
645 "enum": ["a", null, "b"]
646 });
647 let result = adapter.normalize_schema(schema);
648 assert_eq!(result["enum"], json!(["a", "b"]));
649 }
650
651 #[test]
652 fn test_removes_empty_enum_after_null_strip() {
653 let adapter = GeminiSchemaAdapter::new();
654 let schema = json!({
655 "type": "string",
656 "enum": [null]
657 });
658 let result = adapter.normalize_schema(schema);
659 assert!(result.get("enum").is_none());
660 }
661
662 #[test]
663 fn test_adds_implicit_object_type() {
664 let adapter = GeminiSchemaAdapter::new();
665 let schema = json!({
666 "properties": { "name": { "type": "string" } }
667 });
668 let result = adapter.normalize_schema(schema);
669 assert_eq!(result["type"], "object");
670 }
671
672 #[test]
673 fn test_strips_unsupported_formats() {
674 let adapter = GeminiSchemaAdapter::new();
675 let schema = json!({
676 "type": "object",
677 "properties": {
678 "created": { "type": "string", "format": "date-time" },
679 "hostname": { "type": "string", "format": "hostname" },
680 "id": { "type": "string", "format": "uuid" }
681 }
682 });
683 let result = adapter.normalize_schema(schema);
684 assert_eq!(result["properties"]["created"]["format"], "date-time");
685 assert!(result["properties"]["hostname"].get("format").is_none());
686 assert_eq!(result["properties"]["id"]["format"], "uuid");
687 }
688
689 #[test]
690 fn test_preserves_all_allowed_formats() {
691 let adapter = GeminiSchemaAdapter::new();
692 for format in GEMINI_ALLOWED_FORMATS {
693 let schema = json!({ "type": "string", "format": format });
694 let result = adapter.normalize_schema(schema);
695 assert_eq!(result["format"], *format, "format '{format}' should be preserved");
696 }
697 }
698
699 #[test]
700 fn test_enforces_nesting_depth() {
701 let adapter = GeminiSchemaAdapter::new();
702 let schema = json!({
704 "type": "object",
705 "properties": {
706 "l1": {
707 "type": "object",
708 "properties": {
709 "l2": {
710 "type": "object",
711 "properties": {
712 "l3": {
713 "type": "object",
714 "properties": {
715 "l4": {
716 "type": "object",
717 "properties": {
718 "l5": {
719 "type": "object",
720 "properties": {
721 "l6": { "type": "string" }
722 }
723 }
724 }
725 }
726 }
727 }
728 }
729 }
730 }
731 }
732 }
733 });
734 let result = adapter.normalize_schema(schema);
735 let l5 = &result["properties"]["l1"]["properties"]["l2"]["properties"]["l3"]["properties"]
737 ["l4"]["properties"]["l5"];
738 assert_eq!(l5, &json!({"type": "object"}));
739 }
740
741 #[test]
742 fn test_resolves_refs() {
743 let adapter = GeminiSchemaAdapter::new();
744 let schema = json!({
745 "type": "object",
746 "properties": {
747 "address": { "$ref": "#/definitions/Address" }
748 },
749 "definitions": {
750 "Address": {
751 "type": "object",
752 "properties": {
753 "street": { "type": "string" }
754 }
755 }
756 }
757 });
758 let result = adapter.normalize_schema(schema);
759 assert!(result["properties"]["address"].get("$ref").is_none());
761 assert_eq!(result["properties"]["address"]["type"], "object");
762 assert_eq!(result["properties"]["address"]["properties"]["street"]["type"], "string");
763 assert!(result.get("definitions").is_none());
765 }
766
767 #[test]
768 fn test_resolves_dollar_defs() {
769 let adapter = GeminiSchemaAdapter::new();
770 let schema = json!({
771 "type": "object",
772 "properties": {
773 "item": { "$ref": "#/$defs/Item" }
774 },
775 "$defs": {
776 "Item": {
777 "type": "object",
778 "properties": {
779 "name": { "type": "string" }
780 }
781 }
782 }
783 });
784 let result = adapter.normalize_schema(schema);
785 assert!(result["properties"]["item"].get("$ref").is_none());
786 assert_eq!(result["properties"]["item"]["type"], "object");
787 assert!(result.get("$defs").is_none());
788 }
789
790 #[test]
791 fn test_unresolvable_ref_becomes_object() {
792 let adapter = GeminiSchemaAdapter::new();
793 let schema = json!({
794 "type": "object",
795 "properties": {
796 "unknown": { "$ref": "#/definitions/DoesNotExist" }
797 }
798 });
799 let result = adapter.normalize_schema(schema);
800 assert_eq!(result["properties"]["unknown"], json!({"type": "object"}));
801 }
802
803 #[test]
804 fn test_circular_ref_breaks() {
805 let adapter = GeminiSchemaAdapter::new();
806 let schema = json!({
807 "type": "object",
808 "properties": {
809 "self_ref": { "$ref": "#/definitions/Node" }
810 },
811 "definitions": {
812 "Node": {
813 "type": "object",
814 "properties": {
815 "child": { "$ref": "#/definitions/Node" }
816 }
817 }
818 }
819 });
820 let result = adapter.normalize_schema(schema);
821 assert_eq!(result["properties"]["self_ref"]["type"], "object");
823 assert!(result.get("definitions").is_none());
824 }
825
826 #[test]
827 fn test_removes_definitions_and_defs() {
828 let adapter = GeminiSchemaAdapter::new();
829 let schema = json!({
830 "type": "object",
831 "definitions": { "Foo": { "type": "string" } },
832 "$defs": { "Bar": { "type": "number" } }
833 });
834 let result = adapter.normalize_schema(schema);
835 assert!(result.get("definitions").is_none());
836 assert!(result.get("$defs").is_none());
837 }
838
839 #[test]
840 fn test_nested_unsupported_keywords_removed() {
841 let adapter = GeminiSchemaAdapter::new();
842 let schema = json!({
843 "type": "object",
844 "properties": {
845 "inner": {
846 "type": "object",
847 "additionalProperties": false,
848 "exclusiveMinimum": 5,
849 "properties": {
850 "deep": {
851 "type": "number",
852 "exclusiveMaximum": 100
853 }
854 }
855 }
856 }
857 });
858 let result = adapter.normalize_schema(schema);
859 let inner = &result["properties"]["inner"];
860 assert!(inner.get("additionalProperties").is_none());
861 assert!(inner.get("exclusiveMinimum").is_none());
862 assert!(inner["properties"]["deep"].get("exclusiveMaximum").is_none());
863 }
864
865 #[test]
866 fn test_full_transform_pipeline() {
867 let adapter = GeminiSchemaAdapter::new();
868 let schema = json!({
869 "$schema": "http://json-schema.org/draft-07/schema#",
870 "definitions": {
871 "Status": { "type": "string", "enum": ["active", null, "inactive"] }
872 },
873 "properties": {
874 "name": { "type": ["string", "null"], "format": "hostname" },
875 "status": { "$ref": "#/definitions/Status" },
876 "config": {
877 "type": "object",
878 "additionalProperties": true,
879 "properties": {
880 "value": { "const": "fixed" }
881 }
882 }
883 },
884 "if": { "properties": { "name": { "type": "string" } } },
885 "then": { "required": ["status"] },
886 "additionalProperties": false
887 });
888 let result = adapter.normalize_schema(schema);
889
890 assert!(result.get("$schema").is_none());
892 assert!(result.get("definitions").is_none());
894 assert!(result.get("if").is_none());
896 assert!(result.get("then").is_none());
897 assert!(result.get("additionalProperties").is_none());
899 assert_eq!(result["properties"]["name"]["type"], "string");
901 assert!(result["properties"]["name"].get("format").is_none());
903 assert_eq!(result["properties"]["status"]["enum"], json!(["active", "inactive"]));
905 assert_eq!(result["properties"]["config"]["properties"]["value"]["enum"], json!(["fixed"]));
907 assert!(result["properties"]["config"].get("additionalProperties").is_none());
909 assert_eq!(result["type"], "object");
911 }
912
913 #[test]
914 fn test_idempotent() {
915 let adapter = GeminiSchemaAdapter::new();
916 let schema = json!({
917 "$schema": "http://json-schema.org/draft-07/schema#",
918 "type": "object",
919 "properties": {
920 "name": { "type": ["string", "null"], "format": "hostname" },
921 "items": { "type": "array", "items": { "type": "string" } }
922 },
923 "additionalProperties": true,
924 "if": { "const": true },
925 "then": { "required": ["name"] }
926 });
927 let first = adapter.normalize_schema(schema);
928 let second = adapter.normalize_schema(first.clone());
929 assert_eq!(first, second);
930 }
931
932 #[test]
933 fn test_empty_schema() {
934 let adapter = GeminiSchemaAdapter::new();
935 let schema = json!({});
936 let result = adapter.normalize_schema(schema);
937 assert_eq!(result, json!({}));
938 }
939
940 #[test]
941 fn test_array_items_nested_cleanup() {
942 let adapter = GeminiSchemaAdapter::new();
943 let schema = json!({
944 "type": "array",
945 "items": {
946 "type": "object",
947 "additionalProperties": true,
948 "properties": {
949 "id": { "type": "integer", "exclusiveMinimum": 0 }
950 }
951 }
952 });
953 let result = adapter.normalize_schema(schema);
954 assert!(result["items"].get("additionalProperties").is_none());
955 assert!(result["items"]["properties"]["id"].get("exclusiveMinimum").is_none());
956 }
957
958 #[test]
961 fn test_vertex_ai_sets_additional_properties_false() {
962 let adapter = GeminiSchemaAdapter::vertex_ai();
963 let schema = json!({
964 "type": "object",
965 "properties": { "name": { "type": "string" } },
966 "additionalProperties": true
967 });
968 let result = adapter.normalize_schema(schema);
969 assert_eq!(result["additionalProperties"], json!(false));
970 }
971
972 #[test]
973 fn test_vertex_ai_sets_additional_properties_false_on_nested_objects() {
974 let adapter = GeminiSchemaAdapter::vertex_ai();
975 let schema = json!({
976 "type": "object",
977 "properties": {
978 "inner": {
979 "type": "object",
980 "properties": {
981 "value": { "type": "string" }
982 }
983 }
984 }
985 });
986 let result = adapter.normalize_schema(schema);
987 assert_eq!(result["additionalProperties"], json!(false));
988 assert_eq!(result["properties"]["inner"]["additionalProperties"], json!(false));
989 }
990
991 #[test]
992 fn test_vertex_ai_does_not_set_additional_properties_on_non_object() {
993 let adapter = GeminiSchemaAdapter::vertex_ai();
994 let schema = json!({
995 "type": "string",
996 "additionalProperties": true
997 });
998 let result = adapter.normalize_schema(schema);
999 assert!(result.get("additionalProperties").is_none());
1001 }
1002
1003 #[test]
1004 fn test_standard_mode_removes_additional_properties() {
1005 let adapter = GeminiSchemaAdapter::new();
1006 let schema = json!({
1007 "type": "object",
1008 "properties": { "name": { "type": "string" } },
1009 "additionalProperties": true
1010 });
1011 let result = adapter.normalize_schema(schema);
1012 assert!(result.get("additionalProperties").is_none());
1013 }
1014
1015 #[test]
1016 fn test_vertex_ai_still_removes_other_unsupported_keywords() {
1017 let adapter = GeminiSchemaAdapter::vertex_ai();
1018 let schema = json!({
1019 "type": "object",
1020 "properties": { "x": { "type": "number" } },
1021 "exclusiveMinimum": 0,
1022 "exclusiveMaximum": 100,
1023 "not": { "type": "null" },
1024 "propertyNames": { "pattern": "^[a-z]" },
1025 "patternProperties": { "^S_": { "type": "string" } },
1026 "unevaluatedProperties": false
1027 });
1028 let result = adapter.normalize_schema(schema);
1029 assert!(result.get("exclusiveMinimum").is_none());
1030 assert!(result.get("exclusiveMaximum").is_none());
1031 assert!(result.get("not").is_none());
1032 assert!(result.get("propertyNames").is_none());
1033 assert!(result.get("patternProperties").is_none());
1034 assert!(result.get("unevaluatedProperties").is_none());
1035 assert_eq!(result["additionalProperties"], json!(false));
1037 }
1038
1039 #[test]
1042 fn test_normalize_tool_name_short_name_unchanged() {
1043 let adapter = GeminiSchemaAdapter::new();
1044 let name = "get_weather";
1045 let result = adapter.normalize_tool_name(name);
1046 assert_eq!(result, "get_weather");
1047 assert!(matches!(result, Cow::Borrowed(_)));
1048 }
1049
1050 #[test]
1051 fn test_normalize_tool_name_exactly_64_bytes() {
1052 let adapter = GeminiSchemaAdapter::new();
1053 let name = "a".repeat(64);
1054 let result = adapter.normalize_tool_name(&name);
1055 assert_eq!(result.len(), 64);
1056 assert!(matches!(result, Cow::Borrowed(_)));
1057 }
1058
1059 #[test]
1060 fn test_normalize_tool_name_truncates_at_64_bytes() {
1061 let adapter = GeminiSchemaAdapter::new();
1062 let name = "a".repeat(100);
1063 let result = adapter.normalize_tool_name(&name);
1064 assert_eq!(result.len(), 64);
1065 assert_eq!(result.as_ref(), "a".repeat(64));
1066 }
1067
1068 #[test]
1069 fn test_normalize_tool_name_multibyte_boundary() {
1070 let adapter = GeminiSchemaAdapter::new();
1071 let name = "日".repeat(22); let result = adapter.normalize_tool_name(&name);
1075 assert!(result.len() <= 64);
1076 assert_eq!(result.len(), 63);
1078 assert_eq!(result.as_ref(), "日".repeat(21));
1079 assert!(std::str::from_utf8(result.as_bytes()).is_ok());
1081 }
1082
1083 #[test]
1084 fn test_normalize_tool_name_emoji_boundary() {
1085 let adapter = GeminiSchemaAdapter::new();
1086 let name = "🎯".repeat(16);
1088 assert_eq!(name.len(), 64);
1089 let result = adapter.normalize_tool_name(&name);
1090 assert_eq!(result.len(), 64);
1091
1092 let name = "🎯".repeat(17);
1094 let result = adapter.normalize_tool_name(&name);
1095 assert_eq!(result.len(), 64);
1096 assert_eq!(result.as_ref(), "🎯".repeat(16));
1097 }
1098
1099 #[test]
1102 fn test_empty_schema_returns_object_with_properties() {
1103 let adapter = GeminiSchemaAdapter::new();
1104 let result = adapter.empty_schema();
1105 assert_eq!(result, json!({"type": "object", "properties": {}}));
1106 }
1107
1108 #[test]
1109 fn test_empty_schema_vertex_ai_same_as_standard() {
1110 let adapter = GeminiSchemaAdapter::vertex_ai();
1111 let result = adapter.empty_schema();
1112 assert_eq!(result, json!({"type": "object", "properties": {}}));
1113 }
1114
1115 #[test]
1119 fn test_removes_all_validation_keywords() {
1120 let adapter = GeminiSchemaAdapter::new();
1121 let schema = json!({
1122 "type": "object",
1123 "title": "MySchema",
1124 "$id": "https://example.com/schema",
1125 "default": {},
1126 "deprecated": true,
1127 "readOnly": true,
1128 "writeOnly": false,
1129 "examples": [{"name": "test"}],
1130 "minProperties": 1,
1131 "maxProperties": 10,
1132 "properties": {
1133 "name": {
1134 "type": "string",
1135 "title": "Name",
1136 "default": "",
1137 "minLength": 1,
1138 "maxLength": 100,
1139 "pattern": "^[a-z]+$"
1140 },
1141 "age": {
1142 "type": "integer",
1143 "minimum": 0,
1144 "maximum": 150,
1145 "multipleOf": 1
1146 },
1147 "tags": {
1148 "type": "array",
1149 "items": { "type": "string" },
1150 "minItems": 1,
1151 "maxItems": 10,
1152 "uniqueItems": true,
1153 "contains": { "type": "string" }
1154 }
1155 }
1156 });
1157 let result = adapter.normalize_schema(schema);
1158
1159 assert!(result.get("title").is_none());
1161 assert!(result.get("$id").is_none());
1162 assert!(result.get("default").is_none());
1163 assert!(result.get("deprecated").is_none());
1164 assert!(result.get("readOnly").is_none());
1165 assert!(result.get("writeOnly").is_none());
1166 assert!(result.get("examples").is_none());
1167 assert!(result.get("minProperties").is_none());
1168 assert!(result.get("maxProperties").is_none());
1169
1170 let name = &result["properties"]["name"];
1172 assert!(name.get("title").is_none());
1173 assert!(name.get("default").is_none());
1174 assert!(name.get("minLength").is_none());
1175 assert!(name.get("maxLength").is_none());
1176 assert!(name.get("pattern").is_none());
1177 assert_eq!(name["type"], "string");
1178
1179 let age = &result["properties"]["age"];
1181 assert!(age.get("minimum").is_none());
1182 assert!(age.get("maximum").is_none());
1183 assert!(age.get("multipleOf").is_none());
1184 assert_eq!(age["type"], "integer");
1185
1186 let tags = &result["properties"]["tags"];
1188 assert!(tags.get("minItems").is_none());
1189 assert!(tags.get("maxItems").is_none());
1190 assert!(tags.get("uniqueItems").is_none());
1191 assert!(tags.get("contains").is_none());
1192 assert_eq!(tags["type"], "array");
1193 assert_eq!(tags["items"]["type"], "string");
1194 }
1195
1196 #[test]
1197 fn test_removes_prefix_items() {
1198 let adapter = GeminiSchemaAdapter::new();
1199 let schema = json!({
1200 "type": "array",
1201 "prefixItems": [
1202 { "type": "string" },
1203 { "type": "integer" }
1204 ]
1205 });
1206 let result = adapter.normalize_schema(schema);
1207 assert!(result.get("prefixItems").is_none());
1208 }
1209
1210 #[test]
1211 fn test_removes_dependent_keywords() {
1212 let adapter = GeminiSchemaAdapter::new();
1213 let schema = json!({
1214 "type": "object",
1215 "properties": {
1216 "name": { "type": "string" },
1217 "credit_card": { "type": "string" }
1218 },
1219 "dependentRequired": {
1220 "credit_card": ["billing_address"]
1221 },
1222 "dependentSchemas": {
1223 "credit_card": {
1224 "properties": {
1225 "billing_address": { "type": "string" }
1226 }
1227 }
1228 }
1229 });
1230 let result = adapter.normalize_schema(schema);
1231 assert!(result.get("dependentRequired").is_none());
1232 assert!(result.get("dependentSchemas").is_none());
1233 }
1234
1235 #[test]
1236 fn test_removes_content_keywords() {
1237 let adapter = GeminiSchemaAdapter::new();
1238 let schema = json!({
1239 "type": "string",
1240 "contentMediaType": "application/json",
1241 "contentEncoding": "base64"
1242 });
1243 let result = adapter.normalize_schema(schema);
1244 assert!(result.get("contentMediaType").is_none());
1245 assert!(result.get("contentEncoding").is_none());
1246 }
1247}