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] = &[
59 "additionalProperties",
60 "exclusiveMinimum",
61 "exclusiveMaximum",
62 "not",
63 "propertyNames",
64 "patternProperties",
65 "unevaluatedProperties",
66];
67
68const UNSUPPORTED_KEYWORDS_VERTEX: &[&str] = &[
72 "exclusiveMinimum",
73 "exclusiveMaximum",
74 "not",
75 "propertyNames",
76 "patternProperties",
77 "unevaluatedProperties",
78];
79
80#[derive(Debug)]
109pub struct GeminiSchemaAdapter {
110 vertex_ai: bool,
113}
114
115impl GeminiSchemaAdapter {
116 pub fn new() -> Self {
120 Self { vertex_ai: false }
121 }
122
123 pub fn vertex_ai() -> Self {
128 Self { vertex_ai: true }
129 }
130}
131
132impl Default for GeminiSchemaAdapter {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138impl SchemaAdapter for GeminiSchemaAdapter {
139 fn normalize_schema(&self, mut schema: Value) -> Value {
140 let definitions = extract_definitions(&schema);
144 schema_utils::resolve_refs(&mut schema, &definitions, 0);
145
146 schema_utils::strip_schema_keyword(&mut schema);
148
149 schema_utils::collapse_combiners(&mut schema);
151
152 schema_utils::merge_all_of(&mut schema);
154
155 schema_utils::collapse_type_arrays(&mut schema);
157
158 schema_utils::strip_conditional_keywords(&mut schema);
160
161 schema_utils::convert_const_to_enum(&mut schema);
163
164 schema_utils::strip_null_from_enum(&mut schema);
166
167 schema_utils::add_implicit_object_type(&mut schema);
169
170 if self.vertex_ai {
172 remove_unsupported_keywords_vertex(&mut schema);
173 } else {
174 remove_unsupported_keywords(&mut schema);
175 }
176
177 schema_utils::strip_unsupported_formats(&mut schema, GEMINI_ALLOWED_FORMATS);
179
180 schema_utils::enforce_nesting_depth(&mut schema, 5, 0);
182
183 if let Some(obj) = schema.as_object_mut() {
185 obj.remove("definitions");
186 obj.remove("$defs");
187 }
188
189 schema
190 }
191
192 fn normalize_tool_name<'a>(&self, name: &'a str) -> Cow<'a, str> {
196 if name.len() <= 64 {
197 Cow::Borrowed(name)
198 } else {
199 let mut end = 64;
200 while end > 0 && !name.is_char_boundary(end) {
201 end -= 1;
202 }
203 Cow::Owned(name[..end].to_string())
204 }
205 }
206
207 fn empty_schema(&self) -> Value {
212 serde_json::json!({"type": "object", "properties": {}})
213 }
214}
215
216fn extract_definitions(schema: &Value) -> Map<String, Value> {
219 let mut defs = Map::new();
220
221 if let Some(obj) = schema.as_object() {
222 if let Some(definitions) = obj.get("definitions").and_then(|v| v.as_object()) {
224 for (key, value) in definitions {
225 defs.insert(key.clone(), value.clone());
226 }
227 }
228
229 if let Some(dollar_defs) = obj.get("$defs").and_then(|v| v.as_object()) {
231 for (key, value) in dollar_defs {
232 defs.insert(key.clone(), value.clone());
233 }
234 }
235 }
236
237 defs
238}
239
240fn remove_unsupported_keywords(schema: &mut Value) {
246 let Some(obj) = schema.as_object_mut() else {
247 return;
248 };
249
250 for keyword in UNSUPPORTED_KEYWORDS {
252 obj.remove(*keyword);
253 }
254
255 let is_array_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "array");
257 if !is_array_type {
258 obj.remove("items");
259 }
260
261 if let Some(props) = obj.get_mut("properties")
263 && let Some(props_obj) = props.as_object_mut()
264 {
265 for value in props_obj.values_mut() {
266 remove_unsupported_keywords(value);
267 }
268 }
269
270 if let Some(items) = obj.get_mut("items") {
272 if items.is_object() {
273 remove_unsupported_keywords(items);
274 } else if let Some(arr) = items.as_array_mut() {
275 for item in arr.iter_mut() {
276 remove_unsupported_keywords(item);
277 }
278 }
279 }
280
281 for keyword in &["allOf", "anyOf", "oneOf"] {
283 if let Some(arr_val) = obj.get_mut(*keyword)
284 && let Some(arr) = arr_val.as_array_mut()
285 {
286 for sub in arr.iter_mut() {
287 remove_unsupported_keywords(sub);
288 }
289 }
290 }
291
292 if let Some(prefix_items) = obj.get_mut("prefixItems")
294 && let Some(arr) = prefix_items.as_array_mut()
295 {
296 for item in arr.iter_mut() {
297 remove_unsupported_keywords(item);
298 }
299 }
300}
301
302fn remove_unsupported_keywords_vertex(schema: &mut Value) {
309 let Some(obj) = schema.as_object_mut() else {
310 return;
311 };
312
313 for keyword in UNSUPPORTED_KEYWORDS_VERTEX {
315 obj.remove(*keyword);
316 }
317
318 let is_object_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "object");
320 if is_object_type {
321 obj.insert("additionalProperties".to_string(), Value::Bool(false));
322 } else {
323 obj.remove("additionalProperties");
325 }
326
327 let is_array_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "array");
329 if !is_array_type {
330 obj.remove("items");
331 }
332
333 if let Some(props) = obj.get_mut("properties")
335 && let Some(props_obj) = props.as_object_mut()
336 {
337 for value in props_obj.values_mut() {
338 remove_unsupported_keywords_vertex(value);
339 }
340 }
341
342 if let Some(items) = obj.get_mut("items") {
344 if items.is_object() {
345 remove_unsupported_keywords_vertex(items);
346 } else if let Some(arr) = items.as_array_mut() {
347 for item in arr.iter_mut() {
348 remove_unsupported_keywords_vertex(item);
349 }
350 }
351 }
352
353 for keyword in &["allOf", "anyOf", "oneOf"] {
355 if let Some(arr_val) = obj.get_mut(*keyword)
356 && let Some(arr) = arr_val.as_array_mut()
357 {
358 for sub in arr.iter_mut() {
359 remove_unsupported_keywords_vertex(sub);
360 }
361 }
362 }
363
364 if let Some(prefix_items) = obj.get_mut("prefixItems")
366 && let Some(arr) = prefix_items.as_array_mut()
367 {
368 for item in arr.iter_mut() {
369 remove_unsupported_keywords_vertex(item);
370 }
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use serde_json::json;
378
379 #[test]
380 fn test_strips_schema_keyword() {
381 let adapter = GeminiSchemaAdapter::new();
382 let schema = json!({
383 "$schema": "http://json-schema.org/draft-07/schema#",
384 "type": "object",
385 "properties": { "name": { "type": "string" } }
386 });
387 let result = adapter.normalize_schema(schema);
388 assert!(result.get("$schema").is_none());
389 }
390
391 #[test]
392 fn test_removes_additional_properties() {
393 let adapter = GeminiSchemaAdapter::new();
394 let schema = json!({
395 "type": "object",
396 "properties": { "name": { "type": "string" } },
397 "additionalProperties": true
398 });
399 let result = adapter.normalize_schema(schema);
400 assert!(result.get("additionalProperties").is_none());
401 }
402
403 #[test]
404 fn test_removes_exclusive_min_max() {
405 let adapter = GeminiSchemaAdapter::new();
406 let schema = json!({
407 "type": "number",
408 "exclusiveMinimum": 0,
409 "exclusiveMaximum": 100
410 });
411 let result = adapter.normalize_schema(schema);
412 assert!(result.get("exclusiveMinimum").is_none());
413 assert!(result.get("exclusiveMaximum").is_none());
414 }
415
416 #[test]
417 fn test_removes_items_when_not_array() {
418 let adapter = GeminiSchemaAdapter::new();
419 let schema = json!({
420 "type": "object",
421 "items": { "type": "string" }
422 });
423 let result = adapter.normalize_schema(schema);
424 assert!(result.get("items").is_none());
425 }
426
427 #[test]
428 fn test_preserves_items_when_array() {
429 let adapter = GeminiSchemaAdapter::new();
430 let schema = json!({
431 "type": "array",
432 "items": { "type": "string" }
433 });
434 let result = adapter.normalize_schema(schema);
435 assert!(result.get("items").is_some());
436 assert_eq!(result["items"]["type"], "string");
437 }
438
439 #[test]
440 fn test_removes_not_keyword() {
441 let adapter = GeminiSchemaAdapter::new();
442 let schema = json!({
443 "type": "string",
444 "not": { "enum": ["bad"] }
445 });
446 let result = adapter.normalize_schema(schema);
447 assert!(result.get("not").is_none());
448 }
449
450 #[test]
451 fn test_removes_property_names() {
452 let adapter = GeminiSchemaAdapter::new();
453 let schema = json!({
454 "type": "object",
455 "propertyNames": { "pattern": "^[a-z]+$" }
456 });
457 let result = adapter.normalize_schema(schema);
458 assert!(result.get("propertyNames").is_none());
459 }
460
461 #[test]
462 fn test_removes_pattern_properties() {
463 let adapter = GeminiSchemaAdapter::new();
464 let schema = json!({
465 "type": "object",
466 "patternProperties": { "^S_": { "type": "string" } }
467 });
468 let result = adapter.normalize_schema(schema);
469 assert!(result.get("patternProperties").is_none());
470 }
471
472 #[test]
473 fn test_removes_unevaluated_properties() {
474 let adapter = GeminiSchemaAdapter::new();
475 let schema = json!({
476 "type": "object",
477 "unevaluatedProperties": false
478 });
479 let result = adapter.normalize_schema(schema);
480 assert!(result.get("unevaluatedProperties").is_none());
481 }
482
483 #[test]
484 fn test_collapses_any_of() {
485 let adapter = GeminiSchemaAdapter::new();
486 let schema = json!({
487 "anyOf": [
488 { "type": "null" },
489 { "type": "string", "minLength": 1 }
490 ]
491 });
492 let result = adapter.normalize_schema(schema);
493 assert!(result.get("anyOf").is_none());
494 assert_eq!(result["type"], "string");
495 assert_eq!(result["minLength"], 1);
496 }
497
498 #[test]
499 fn test_collapses_one_of() {
500 let adapter = GeminiSchemaAdapter::new();
501 let schema = json!({
502 "oneOf": [
503 { "type": "null" },
504 { "type": "integer", "minimum": 0 }
505 ]
506 });
507 let result = adapter.normalize_schema(schema);
508 assert!(result.get("oneOf").is_none());
509 assert_eq!(result["type"], "integer");
510 }
511
512 #[test]
513 fn test_merges_all_of() {
514 let adapter = GeminiSchemaAdapter::new();
515 let schema = json!({
516 "allOf": [
517 { "type": "object", "properties": { "a": { "type": "string" } } },
518 { "properties": { "b": { "type": "number" } }, "required": ["b"] }
519 ]
520 });
521 let result = adapter.normalize_schema(schema);
522 assert!(result.get("allOf").is_none());
523 assert_eq!(result["properties"]["a"]["type"], "string");
524 assert_eq!(result["properties"]["b"]["type"], "number");
525 assert_eq!(result["required"], json!(["b"]));
526 }
527
528 #[test]
529 fn test_collapses_type_arrays() {
530 let adapter = GeminiSchemaAdapter::new();
531 let schema = json!({
532 "type": ["string", "null"],
533 "minLength": 1
534 });
535 let result = adapter.normalize_schema(schema);
536 assert_eq!(result["type"], "string");
537 }
538
539 #[test]
540 fn test_strips_conditional_keywords() {
541 let adapter = GeminiSchemaAdapter::new();
542 let schema = json!({
543 "type": "object",
544 "if": { "properties": { "kind": { "const": "a" } } },
545 "then": { "required": ["extra"] },
546 "else": { "required": [] }
547 });
548 let result = adapter.normalize_schema(schema);
549 assert!(result.get("if").is_none());
550 assert!(result.get("then").is_none());
551 assert!(result.get("else").is_none());
552 }
553
554 #[test]
555 fn test_converts_const_to_enum() {
556 let adapter = GeminiSchemaAdapter::new();
557 let schema = json!({
558 "type": "string",
559 "const": "fixed"
560 });
561 let result = adapter.normalize_schema(schema);
562 assert!(result.get("const").is_none());
563 assert_eq!(result["enum"], json!(["fixed"]));
564 }
565
566 #[test]
567 fn test_strips_null_from_enum() {
568 let adapter = GeminiSchemaAdapter::new();
569 let schema = json!({
570 "type": "string",
571 "enum": ["a", null, "b"]
572 });
573 let result = adapter.normalize_schema(schema);
574 assert_eq!(result["enum"], json!(["a", "b"]));
575 }
576
577 #[test]
578 fn test_removes_empty_enum_after_null_strip() {
579 let adapter = GeminiSchemaAdapter::new();
580 let schema = json!({
581 "type": "string",
582 "enum": [null]
583 });
584 let result = adapter.normalize_schema(schema);
585 assert!(result.get("enum").is_none());
586 }
587
588 #[test]
589 fn test_adds_implicit_object_type() {
590 let adapter = GeminiSchemaAdapter::new();
591 let schema = json!({
592 "properties": { "name": { "type": "string" } }
593 });
594 let result = adapter.normalize_schema(schema);
595 assert_eq!(result["type"], "object");
596 }
597
598 #[test]
599 fn test_strips_unsupported_formats() {
600 let adapter = GeminiSchemaAdapter::new();
601 let schema = json!({
602 "type": "object",
603 "properties": {
604 "created": { "type": "string", "format": "date-time" },
605 "hostname": { "type": "string", "format": "hostname" },
606 "id": { "type": "string", "format": "uuid" }
607 }
608 });
609 let result = adapter.normalize_schema(schema);
610 assert_eq!(result["properties"]["created"]["format"], "date-time");
611 assert!(result["properties"]["hostname"].get("format").is_none());
612 assert_eq!(result["properties"]["id"]["format"], "uuid");
613 }
614
615 #[test]
616 fn test_preserves_all_allowed_formats() {
617 let adapter = GeminiSchemaAdapter::new();
618 for format in GEMINI_ALLOWED_FORMATS {
619 let schema = json!({ "type": "string", "format": format });
620 let result = adapter.normalize_schema(schema);
621 assert_eq!(result["format"], *format, "format '{format}' should be preserved");
622 }
623 }
624
625 #[test]
626 fn test_enforces_nesting_depth() {
627 let adapter = GeminiSchemaAdapter::new();
628 let schema = json!({
630 "type": "object",
631 "properties": {
632 "l1": {
633 "type": "object",
634 "properties": {
635 "l2": {
636 "type": "object",
637 "properties": {
638 "l3": {
639 "type": "object",
640 "properties": {
641 "l4": {
642 "type": "object",
643 "properties": {
644 "l5": {
645 "type": "object",
646 "properties": {
647 "l6": { "type": "string" }
648 }
649 }
650 }
651 }
652 }
653 }
654 }
655 }
656 }
657 }
658 }
659 });
660 let result = adapter.normalize_schema(schema);
661 let l5 = &result["properties"]["l1"]["properties"]["l2"]["properties"]["l3"]["properties"]
663 ["l4"]["properties"]["l5"];
664 assert_eq!(l5, &json!({"type": "object"}));
665 }
666
667 #[test]
668 fn test_resolves_refs() {
669 let adapter = GeminiSchemaAdapter::new();
670 let schema = json!({
671 "type": "object",
672 "properties": {
673 "address": { "$ref": "#/definitions/Address" }
674 },
675 "definitions": {
676 "Address": {
677 "type": "object",
678 "properties": {
679 "street": { "type": "string" }
680 }
681 }
682 }
683 });
684 let result = adapter.normalize_schema(schema);
685 assert!(result["properties"]["address"].get("$ref").is_none());
687 assert_eq!(result["properties"]["address"]["type"], "object");
688 assert_eq!(result["properties"]["address"]["properties"]["street"]["type"], "string");
689 assert!(result.get("definitions").is_none());
691 }
692
693 #[test]
694 fn test_resolves_dollar_defs() {
695 let adapter = GeminiSchemaAdapter::new();
696 let schema = json!({
697 "type": "object",
698 "properties": {
699 "item": { "$ref": "#/$defs/Item" }
700 },
701 "$defs": {
702 "Item": {
703 "type": "object",
704 "properties": {
705 "name": { "type": "string" }
706 }
707 }
708 }
709 });
710 let result = adapter.normalize_schema(schema);
711 assert!(result["properties"]["item"].get("$ref").is_none());
712 assert_eq!(result["properties"]["item"]["type"], "object");
713 assert!(result.get("$defs").is_none());
714 }
715
716 #[test]
717 fn test_unresolvable_ref_becomes_object() {
718 let adapter = GeminiSchemaAdapter::new();
719 let schema = json!({
720 "type": "object",
721 "properties": {
722 "unknown": { "$ref": "#/definitions/DoesNotExist" }
723 }
724 });
725 let result = adapter.normalize_schema(schema);
726 assert_eq!(result["properties"]["unknown"], json!({"type": "object"}));
727 }
728
729 #[test]
730 fn test_circular_ref_breaks() {
731 let adapter = GeminiSchemaAdapter::new();
732 let schema = json!({
733 "type": "object",
734 "properties": {
735 "self_ref": { "$ref": "#/definitions/Node" }
736 },
737 "definitions": {
738 "Node": {
739 "type": "object",
740 "properties": {
741 "child": { "$ref": "#/definitions/Node" }
742 }
743 }
744 }
745 });
746 let result = adapter.normalize_schema(schema);
747 assert_eq!(result["properties"]["self_ref"]["type"], "object");
749 assert!(result.get("definitions").is_none());
750 }
751
752 #[test]
753 fn test_removes_definitions_and_defs() {
754 let adapter = GeminiSchemaAdapter::new();
755 let schema = json!({
756 "type": "object",
757 "definitions": { "Foo": { "type": "string" } },
758 "$defs": { "Bar": { "type": "number" } }
759 });
760 let result = adapter.normalize_schema(schema);
761 assert!(result.get("definitions").is_none());
762 assert!(result.get("$defs").is_none());
763 }
764
765 #[test]
766 fn test_nested_unsupported_keywords_removed() {
767 let adapter = GeminiSchemaAdapter::new();
768 let schema = json!({
769 "type": "object",
770 "properties": {
771 "inner": {
772 "type": "object",
773 "additionalProperties": false,
774 "exclusiveMinimum": 5,
775 "properties": {
776 "deep": {
777 "type": "number",
778 "exclusiveMaximum": 100
779 }
780 }
781 }
782 }
783 });
784 let result = adapter.normalize_schema(schema);
785 let inner = &result["properties"]["inner"];
786 assert!(inner.get("additionalProperties").is_none());
787 assert!(inner.get("exclusiveMinimum").is_none());
788 assert!(inner["properties"]["deep"].get("exclusiveMaximum").is_none());
789 }
790
791 #[test]
792 fn test_full_transform_pipeline() {
793 let adapter = GeminiSchemaAdapter::new();
794 let schema = json!({
795 "$schema": "http://json-schema.org/draft-07/schema#",
796 "definitions": {
797 "Status": { "type": "string", "enum": ["active", null, "inactive"] }
798 },
799 "properties": {
800 "name": { "type": ["string", "null"], "format": "hostname" },
801 "status": { "$ref": "#/definitions/Status" },
802 "config": {
803 "type": "object",
804 "additionalProperties": true,
805 "properties": {
806 "value": { "const": "fixed" }
807 }
808 }
809 },
810 "if": { "properties": { "name": { "type": "string" } } },
811 "then": { "required": ["status"] },
812 "additionalProperties": false
813 });
814 let result = adapter.normalize_schema(schema);
815
816 assert!(result.get("$schema").is_none());
818 assert!(result.get("definitions").is_none());
820 assert!(result.get("if").is_none());
822 assert!(result.get("then").is_none());
823 assert!(result.get("additionalProperties").is_none());
825 assert_eq!(result["properties"]["name"]["type"], "string");
827 assert!(result["properties"]["name"].get("format").is_none());
829 assert_eq!(result["properties"]["status"]["enum"], json!(["active", "inactive"]));
831 assert_eq!(result["properties"]["config"]["properties"]["value"]["enum"], json!(["fixed"]));
833 assert!(result["properties"]["config"].get("additionalProperties").is_none());
835 assert_eq!(result["type"], "object");
837 }
838
839 #[test]
840 fn test_idempotent() {
841 let adapter = GeminiSchemaAdapter::new();
842 let schema = json!({
843 "$schema": "http://json-schema.org/draft-07/schema#",
844 "type": "object",
845 "properties": {
846 "name": { "type": ["string", "null"], "format": "hostname" },
847 "items": { "type": "array", "items": { "type": "string" } }
848 },
849 "additionalProperties": true,
850 "if": { "const": true },
851 "then": { "required": ["name"] }
852 });
853 let first = adapter.normalize_schema(schema);
854 let second = adapter.normalize_schema(first.clone());
855 assert_eq!(first, second);
856 }
857
858 #[test]
859 fn test_empty_schema() {
860 let adapter = GeminiSchemaAdapter::new();
861 let schema = json!({});
862 let result = adapter.normalize_schema(schema);
863 assert_eq!(result, json!({}));
864 }
865
866 #[test]
867 fn test_array_items_nested_cleanup() {
868 let adapter = GeminiSchemaAdapter::new();
869 let schema = json!({
870 "type": "array",
871 "items": {
872 "type": "object",
873 "additionalProperties": true,
874 "properties": {
875 "id": { "type": "integer", "exclusiveMinimum": 0 }
876 }
877 }
878 });
879 let result = adapter.normalize_schema(schema);
880 assert!(result["items"].get("additionalProperties").is_none());
881 assert!(result["items"]["properties"]["id"].get("exclusiveMinimum").is_none());
882 }
883
884 #[test]
887 fn test_vertex_ai_sets_additional_properties_false() {
888 let adapter = GeminiSchemaAdapter::vertex_ai();
889 let schema = json!({
890 "type": "object",
891 "properties": { "name": { "type": "string" } },
892 "additionalProperties": true
893 });
894 let result = adapter.normalize_schema(schema);
895 assert_eq!(result["additionalProperties"], json!(false));
896 }
897
898 #[test]
899 fn test_vertex_ai_sets_additional_properties_false_on_nested_objects() {
900 let adapter = GeminiSchemaAdapter::vertex_ai();
901 let schema = json!({
902 "type": "object",
903 "properties": {
904 "inner": {
905 "type": "object",
906 "properties": {
907 "value": { "type": "string" }
908 }
909 }
910 }
911 });
912 let result = adapter.normalize_schema(schema);
913 assert_eq!(result["additionalProperties"], json!(false));
914 assert_eq!(result["properties"]["inner"]["additionalProperties"], json!(false));
915 }
916
917 #[test]
918 fn test_vertex_ai_does_not_set_additional_properties_on_non_object() {
919 let adapter = GeminiSchemaAdapter::vertex_ai();
920 let schema = json!({
921 "type": "string",
922 "additionalProperties": true
923 });
924 let result = adapter.normalize_schema(schema);
925 assert!(result.get("additionalProperties").is_none());
927 }
928
929 #[test]
930 fn test_standard_mode_removes_additional_properties() {
931 let adapter = GeminiSchemaAdapter::new();
932 let schema = json!({
933 "type": "object",
934 "properties": { "name": { "type": "string" } },
935 "additionalProperties": true
936 });
937 let result = adapter.normalize_schema(schema);
938 assert!(result.get("additionalProperties").is_none());
939 }
940
941 #[test]
942 fn test_vertex_ai_still_removes_other_unsupported_keywords() {
943 let adapter = GeminiSchemaAdapter::vertex_ai();
944 let schema = json!({
945 "type": "object",
946 "properties": { "x": { "type": "number" } },
947 "exclusiveMinimum": 0,
948 "exclusiveMaximum": 100,
949 "not": { "type": "null" },
950 "propertyNames": { "pattern": "^[a-z]" },
951 "patternProperties": { "^S_": { "type": "string" } },
952 "unevaluatedProperties": false
953 });
954 let result = adapter.normalize_schema(schema);
955 assert!(result.get("exclusiveMinimum").is_none());
956 assert!(result.get("exclusiveMaximum").is_none());
957 assert!(result.get("not").is_none());
958 assert!(result.get("propertyNames").is_none());
959 assert!(result.get("patternProperties").is_none());
960 assert!(result.get("unevaluatedProperties").is_none());
961 assert_eq!(result["additionalProperties"], json!(false));
963 }
964
965 #[test]
968 fn test_normalize_tool_name_short_name_unchanged() {
969 let adapter = GeminiSchemaAdapter::new();
970 let name = "get_weather";
971 let result = adapter.normalize_tool_name(name);
972 assert_eq!(result, "get_weather");
973 assert!(matches!(result, Cow::Borrowed(_)));
974 }
975
976 #[test]
977 fn test_normalize_tool_name_exactly_64_bytes() {
978 let adapter = GeminiSchemaAdapter::new();
979 let name = "a".repeat(64);
980 let result = adapter.normalize_tool_name(&name);
981 assert_eq!(result.len(), 64);
982 assert!(matches!(result, Cow::Borrowed(_)));
983 }
984
985 #[test]
986 fn test_normalize_tool_name_truncates_at_64_bytes() {
987 let adapter = GeminiSchemaAdapter::new();
988 let name = "a".repeat(100);
989 let result = adapter.normalize_tool_name(&name);
990 assert_eq!(result.len(), 64);
991 assert_eq!(result.as_ref(), "a".repeat(64));
992 }
993
994 #[test]
995 fn test_normalize_tool_name_multibyte_boundary() {
996 let adapter = GeminiSchemaAdapter::new();
997 let name = "日".repeat(22); let result = adapter.normalize_tool_name(&name);
1001 assert!(result.len() <= 64);
1002 assert_eq!(result.len(), 63);
1004 assert_eq!(result.as_ref(), "日".repeat(21));
1005 assert!(std::str::from_utf8(result.as_bytes()).is_ok());
1007 }
1008
1009 #[test]
1010 fn test_normalize_tool_name_emoji_boundary() {
1011 let adapter = GeminiSchemaAdapter::new();
1012 let name = "🎯".repeat(16);
1014 assert_eq!(name.len(), 64);
1015 let result = adapter.normalize_tool_name(&name);
1016 assert_eq!(result.len(), 64);
1017
1018 let name = "🎯".repeat(17);
1020 let result = adapter.normalize_tool_name(&name);
1021 assert_eq!(result.len(), 64);
1022 assert_eq!(result.as_ref(), "🎯".repeat(16));
1023 }
1024
1025 #[test]
1028 fn test_empty_schema_returns_object_with_properties() {
1029 let adapter = GeminiSchemaAdapter::new();
1030 let result = adapter.empty_schema();
1031 assert_eq!(result, json!({"type": "object", "properties": {}}));
1032 }
1033
1034 #[test]
1035 fn test_empty_schema_vertex_ai_same_as_standard() {
1036 let adapter = GeminiSchemaAdapter::vertex_ai();
1037 let result = adapter.empty_schema();
1038 assert_eq!(result, json!({"type": "object", "properties": {}}));
1039 }
1040}