1use jsonschema::error::{TypeKind, ValidationErrorKind};
108use schemars::schema_for;
109use serde::{Serialize, Serializer};
110use serde_json::{Value, json};
111use std::collections::{BTreeMap, HashMap, HashSet};
112
113use crate::HttpClient;
114use crate::error::{
115 Error, ErrorResponse, ToolCallValidationError, ValidationConstraint, ValidationError,
116};
117use crate::tool::ToolMetadata;
118use oas3::spec::{
119 BooleanSchema, ObjectOrReference, ObjectSchema, Operation, Parameter, ParameterIn,
120 ParameterStyle, RequestBody, Response, Schema, SchemaType, SchemaTypeSet, Spec,
121};
122use tracing::{trace, warn};
123
124const X_LOCATION: &str = "x-location";
126const X_PARAMETER_LOCATION: &str = "x-parameter-location";
127const X_PARAMETER_REQUIRED: &str = "x-parameter-required";
128const X_CONTENT_TYPE: &str = "x-content-type";
129const X_ORIGINAL_NAME: &str = "x-original-name";
130const X_PARAMETER_EXPLODE: &str = "x-parameter-explode";
131const X_FILE_FIELDS: &str = "x-file-fields";
132
133#[derive(Debug, Clone, Copy, PartialEq)]
135pub enum Location {
136 Parameter(ParameterIn),
138 Body,
140}
141
142impl Serialize for Location {
143 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
144 where
145 S: Serializer,
146 {
147 let str_value = match self {
148 Location::Parameter(param_in) => match param_in {
149 ParameterIn::Query => "query",
150 ParameterIn::Header => "header",
151 ParameterIn::Path => "path",
152 ParameterIn::Cookie => "cookie",
153 },
154 Location::Body => "body",
155 };
156 serializer.serialize_str(str_value)
157 }
158}
159
160#[derive(Debug, Clone, PartialEq)]
162pub enum Annotation {
163 Location(Location),
165 Required(bool),
167 ContentType(String),
169 OriginalName(String),
171 Explode(bool),
173 FileFields(Vec<String>),
175}
176
177#[derive(Debug, Clone, Default)]
179pub struct Annotations {
180 annotations: Vec<Annotation>,
181}
182
183impl Annotations {
184 pub fn new() -> Self {
186 Self {
187 annotations: Vec::new(),
188 }
189 }
190
191 pub fn with_location(mut self, location: Location) -> Self {
193 self.annotations.push(Annotation::Location(location));
194 self
195 }
196
197 pub fn with_required(mut self, required: bool) -> Self {
199 self.annotations.push(Annotation::Required(required));
200 self
201 }
202
203 pub fn with_content_type(mut self, content_type: String) -> Self {
205 self.annotations.push(Annotation::ContentType(content_type));
206 self
207 }
208
209 pub fn with_original_name(mut self, original_name: String) -> Self {
211 self.annotations
212 .push(Annotation::OriginalName(original_name));
213 self
214 }
215
216 pub fn with_explode(mut self, explode: bool) -> Self {
218 self.annotations.push(Annotation::Explode(explode));
219 self
220 }
221
222 pub fn with_file_fields(mut self, file_fields: Vec<String>) -> Self {
224 self.annotations.push(Annotation::FileFields(file_fields));
225 self
226 }
227}
228
229impl Serialize for Annotations {
230 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
231 where
232 S: Serializer,
233 {
234 use serde::ser::SerializeMap;
235
236 let mut map = serializer.serialize_map(Some(self.annotations.len()))?;
237
238 for annotation in &self.annotations {
239 match annotation {
240 Annotation::Location(location) => {
241 let key = match location {
243 Location::Parameter(param_in) => match param_in {
244 ParameterIn::Header | ParameterIn::Cookie => X_LOCATION,
245 _ => X_PARAMETER_LOCATION,
246 },
247 Location::Body => X_LOCATION,
248 };
249 map.serialize_entry(key, &location)?;
250
251 if let Location::Parameter(_) = location {
253 map.serialize_entry(X_PARAMETER_LOCATION, &location)?;
254 }
255 }
256 Annotation::Required(required) => {
257 map.serialize_entry(X_PARAMETER_REQUIRED, required)?;
258 }
259 Annotation::ContentType(content_type) => {
260 map.serialize_entry(X_CONTENT_TYPE, content_type)?;
261 }
262 Annotation::OriginalName(original_name) => {
263 map.serialize_entry(X_ORIGINAL_NAME, original_name)?;
264 }
265 Annotation::Explode(explode) => {
266 map.serialize_entry(X_PARAMETER_EXPLODE, explode)?;
267 }
268 Annotation::FileFields(file_fields) => {
269 map.serialize_entry(X_FILE_FIELDS, file_fields)?;
270 }
271 }
272 }
273
274 map.end()
275 }
276}
277
278fn sanitize_property_name(name: &str) -> String {
287 let sanitized = name
289 .chars()
290 .map(|c| match c {
291 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '.' | '-' => c,
292 _ => '_',
293 })
294 .take(64)
295 .collect::<String>();
296
297 let mut collapsed = String::with_capacity(sanitized.len());
299 let mut prev_was_underscore = false;
300
301 for ch in sanitized.chars() {
302 if ch == '_' {
303 if !prev_was_underscore {
304 collapsed.push(ch);
305 }
306 prev_was_underscore = true;
307 } else {
308 collapsed.push(ch);
309 prev_was_underscore = false;
310 }
311 }
312
313 let trimmed = collapsed.trim_end_matches('_');
315
316 if trimmed.is_empty() || trimmed.chars().next().unwrap_or('0').is_numeric() {
318 format!("param_{trimmed}")
319 } else {
320 trimmed.to_string()
321 }
322}
323
324#[derive(Debug, Clone, Default)]
388pub struct ReferenceMetadata {
389 pub summary: Option<String>,
396
397 pub description: Option<String>,
404}
405
406impl ReferenceMetadata {
407 pub fn new(summary: Option<String>, description: Option<String>) -> Self {
409 Self {
410 summary,
411 description,
412 }
413 }
414
415 pub fn is_empty(&self) -> bool {
417 self.summary.is_none() && self.description.is_none()
418 }
419
420 pub fn best_description(&self) -> Option<&str> {
470 self.description.as_deref().or(self.summary.as_deref())
471 }
472
473 pub fn summary(&self) -> Option<&str> {
520 self.summary.as_deref()
521 }
522
523 pub fn merge_with_description(
609 &self,
610 existing_desc: Option<&str>,
611 prepend_summary: bool,
612 ) -> Option<String> {
613 match (self.best_description(), self.summary(), existing_desc) {
614 (Some(ref_desc), _, _) => Some(ref_desc.to_string()),
616
617 (None, Some(ref_summary), Some(existing)) if prepend_summary => {
619 if ref_summary != existing {
620 Some(format!("{}\n\n{}", ref_summary, existing))
621 } else {
622 Some(existing.to_string())
623 }
624 }
625 (None, Some(ref_summary), _) => Some(ref_summary.to_string()),
626
627 (None, None, Some(existing)) => Some(existing.to_string()),
629
630 (None, None, None) => None,
632 }
633 }
634
635 pub fn enhance_parameter_description(
721 &self,
722 param_name: &str,
723 existing_desc: Option<&str>,
724 ) -> Option<String> {
725 match (self.best_description(), self.summary(), existing_desc) {
726 (Some(ref_desc), _, _) => Some(format!("{}: {}", param_name, ref_desc)),
728
729 (None, Some(ref_summary), _) => Some(format!("{}: {}", param_name, ref_summary)),
731
732 (None, None, Some(existing)) => Some(existing.to_string()),
734
735 (None, None, None) => Some(format!("{} parameter", param_name)),
737 }
738 }
739}
740
741pub struct ToolGenerator;
743
744impl ToolGenerator {
745 pub fn generate_tool_metadata(
751 operation: &Operation,
752 method: String,
753 path: String,
754 spec: &Spec,
755 skip_tool_description: bool,
756 skip_parameter_descriptions: bool,
757 parameter_examples_in_description: bool,
758 ) -> Result<ToolMetadata, Error> {
759 let name = operation.operation_id.clone().unwrap_or_else(|| {
760 format!(
761 "{}_{}",
762 method,
763 path.replace('/', "_").replace(['{', '}'], "")
764 )
765 });
766
767 let (parameters, parameter_mappings) = Self::generate_parameter_schema(
769 &operation.parameters,
770 &method,
771 &operation.request_body,
772 spec,
773 skip_parameter_descriptions,
774 parameter_examples_in_description,
775 )?;
776
777 let description =
779 (!skip_tool_description).then(|| Self::build_description(operation, &method, &path));
780
781 let output_schema = Self::extract_output_schema(&operation.responses, spec)?;
783
784 Ok(ToolMetadata {
785 name,
786 title: operation.summary.clone(),
787 description,
788 parameters,
789 output_schema,
790 method,
791 path,
792 security: None, parameter_mappings,
794 })
795 }
796
797 pub fn generate_openapi_tools(
803 tools_metadata: Vec<ToolMetadata>,
804 base_url: Option<url::Url>,
805 default_headers: Option<reqwest::header::HeaderMap>,
806 insecure: bool,
807 ) -> Result<Vec<crate::tool::Tool>, Error> {
808 let mut openapi_tools = Vec::with_capacity(tools_metadata.len());
809
810 let mut http_client = HttpClient::new().with_insecure(insecure);
811
812 if let Some(url) = base_url {
813 http_client = http_client.with_base_url(url)?;
814 }
815
816 if let Some(headers) = default_headers {
817 http_client = http_client.with_default_headers(headers);
818 }
819
820 for metadata in tools_metadata {
821 let tool = crate::tool::Tool::new(metadata, http_client.clone())?;
822 openapi_tools.push(tool);
823 }
824
825 Ok(openapi_tools)
826 }
827
828 fn build_description(operation: &Operation, method: &str, path: &str) -> String {
830 match (&operation.summary, &operation.description) {
831 (Some(summary), Some(desc)) => {
832 format!(
833 "{}\n\n{}\n\nEndpoint: {} {}",
834 summary,
835 desc,
836 method.to_uppercase(),
837 path
838 )
839 }
840 (Some(summary), None) => {
841 format!(
842 "{}\n\nEndpoint: {} {}",
843 summary,
844 method.to_uppercase(),
845 path
846 )
847 }
848 (None, Some(desc)) => {
849 format!("{}\n\nEndpoint: {} {}", desc, method.to_uppercase(), path)
850 }
851 (None, None) => {
852 format!("API endpoint: {} {}", method.to_uppercase(), path)
853 }
854 }
855 }
856
857 fn extract_output_schema(
861 responses: &Option<BTreeMap<String, ObjectOrReference<Response>>>,
862 spec: &Spec,
863 ) -> Result<Option<Value>, Error> {
864 let responses = match responses {
865 Some(r) => r,
866 None => return Ok(None),
867 };
868 let priority_codes = vec![
870 "200", "201", "202", "203", "204", "2XX", "default", ];
878
879 for status_code in priority_codes {
880 if let Some(response_or_ref) = responses.get(status_code) {
881 let response = match response_or_ref {
883 ObjectOrReference::Object(response) => response,
884 ObjectOrReference::Ref {
885 ref_path,
886 summary,
887 description,
888 } => {
889 let ref_metadata =
892 ReferenceMetadata::new(summary.clone(), description.clone());
893
894 if let Some(ref_desc) = ref_metadata.best_description() {
895 let response_schema = json!({
897 "type": "object",
898 "description": "Unified response structure with success and error variants",
899 "properties": {
900 "status_code": {
901 "type": "integer",
902 "description": "HTTP status code"
903 },
904 "body": {
905 "type": "object",
906 "description": ref_desc,
907 "additionalProperties": true
908 }
909 },
910 "required": ["status_code", "body"]
911 });
912
913 trace!(
914 reference_path = %ref_path,
915 reference_description = %ref_desc,
916 "Created response schema using reference metadata"
917 );
918
919 return Ok(Some(response_schema));
920 }
921
922 continue;
924 }
925 };
926
927 if status_code == "204" {
929 continue;
930 }
931
932 if !response.content.is_empty() {
934 let content = &response.content;
935 let json_media_types = vec![
937 "application/json",
938 "application/ld+json",
939 "application/vnd.api+json",
940 ];
941
942 for media_type_str in json_media_types {
943 if let Some(media_type) = content.get(media_type_str)
944 && let Some(schema_or_ref) = &media_type.schema
945 {
946 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
948 return Ok(Some(wrapped_schema));
949 }
950 }
951
952 for media_type in content.values() {
954 if let Some(schema_or_ref) = &media_type.schema {
955 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
957 return Ok(Some(wrapped_schema));
958 }
959 }
960 }
961 }
962 }
963
964 Ok(None)
966 }
967
968 fn convert_schema_to_json_schema(
978 schema: &Schema,
979 spec: &Spec,
980 visited: &mut HashSet<String>,
981 ) -> Result<Value, Error> {
982 match schema {
983 Schema::Object(obj_schema_or_ref) => match obj_schema_or_ref.as_ref() {
984 ObjectOrReference::Object(obj_schema) => {
985 Self::convert_object_schema_to_json_schema(obj_schema, spec, visited)
986 }
987 ObjectOrReference::Ref { ref_path, .. } => {
988 let snapshot = visited.clone();
994 let result =
995 Self::resolve_reference(ref_path, spec, visited).and_then(|resolved| {
996 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)
997 });
998 *visited = snapshot;
999 result
1000 }
1001 },
1002 Schema::Boolean(bool_schema) => {
1003 if bool_schema.0 {
1005 Ok(json!({})) } else {
1007 Ok(json!({"not": {}})) }
1009 }
1010 }
1011 }
1012
1013 fn convert_object_schema_to_json_schema(
1023 obj_schema: &ObjectSchema,
1024 spec: &Spec,
1025 visited: &mut HashSet<String>,
1026 ) -> Result<Value, Error> {
1027 let mut schema_obj = serde_json::Map::new();
1028
1029 if let Some(schema_type) = &obj_schema.schema_type {
1031 match schema_type {
1032 SchemaTypeSet::Single(single_type) => {
1033 schema_obj.insert(
1034 "type".to_string(),
1035 json!(Self::schema_type_to_string(single_type)),
1036 );
1037 }
1038 SchemaTypeSet::Multiple(type_set) => {
1039 let types: Vec<String> =
1040 type_set.iter().map(Self::schema_type_to_string).collect();
1041 schema_obj.insert("type".to_string(), json!(types));
1042 }
1043 }
1044 }
1045
1046 if let Some(desc) = &obj_schema.description {
1048 schema_obj.insert("description".to_string(), json!(desc));
1049 }
1050
1051 for schema_ref in &obj_schema.all_of {
1059 let part = Self::convert_member_schema(schema_ref, spec, visited)?;
1060 Self::merge_json_schema(&mut schema_obj, part);
1061 }
1062
1063 if !obj_schema.any_of.is_empty() {
1066 let any_of_schemas = obj_schema
1067 .any_of
1068 .iter()
1069 .map(|schema_ref| Self::convert_member_schema(schema_ref, spec, visited))
1070 .collect::<Result<Vec<_>, _>>()?;
1071 schema_obj.insert("anyOf".to_string(), json!(any_of_schemas));
1072 }
1073
1074 if !obj_schema.one_of.is_empty() {
1076 let one_of_schemas = obj_schema
1077 .one_of
1078 .iter()
1079 .map(|schema_ref| Self::convert_member_schema(schema_ref, spec, visited))
1080 .collect::<Result<Vec<_>, _>>()?;
1081 schema_obj.insert("oneOf".to_string(), json!(one_of_schemas));
1082 return Ok(Value::Object(schema_obj));
1085 }
1086
1087 if !obj_schema.properties.is_empty() {
1089 let properties = &obj_schema.properties;
1090 let mut props_map = serde_json::Map::new();
1091 for (prop_name, prop_schema_or_ref) in properties {
1092 let prop_schema = Self::convert_member_schema(prop_schema_or_ref, spec, visited)?;
1093
1094 let sanitized_name = sanitize_property_name(prop_name);
1096 props_map.insert(sanitized_name, prop_schema);
1097 }
1098 schema_obj.insert("properties".to_string(), Value::Object(props_map));
1099 }
1100
1101 if !obj_schema.required.is_empty() {
1103 schema_obj.insert("required".to_string(), json!(&obj_schema.required));
1104 }
1105
1106 if let Some(schema_type) = &obj_schema.schema_type
1108 && matches!(schema_type, SchemaTypeSet::Single(SchemaType::Object))
1109 {
1110 match &obj_schema.additional_properties {
1112 None => {
1113 schema_obj.insert("additionalProperties".to_string(), json!(true));
1115 }
1116 Some(Schema::Boolean(BooleanSchema(value))) => {
1117 schema_obj.insert("additionalProperties".to_string(), json!(value));
1119 }
1120 Some(Schema::Object(schema_ref)) => {
1121 let additional_props_schema = Self::convert_schema_to_json_schema(
1123 &Schema::Object(schema_ref.clone()),
1124 spec,
1125 visited,
1126 )?;
1127 schema_obj.insert("additionalProperties".to_string(), additional_props_schema);
1128 }
1129 }
1130 }
1131
1132 if let Some(schema_type) = &obj_schema.schema_type {
1134 if matches!(schema_type, SchemaTypeSet::Single(SchemaType::Array)) {
1135 if !obj_schema.prefix_items.is_empty() {
1137 Self::convert_prefix_items_to_draft07(
1139 &obj_schema.prefix_items,
1140 &obj_schema.items,
1141 &mut schema_obj,
1142 spec,
1143 )?;
1144 } else if let Some(items_schema) = &obj_schema.items {
1145 let items_json =
1147 Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1148 schema_obj.insert("items".to_string(), items_json);
1149 }
1150
1151 if let Some(min_items) = obj_schema.min_items {
1153 schema_obj.insert("minItems".to_string(), json!(min_items));
1154 }
1155 if let Some(max_items) = obj_schema.max_items {
1156 schema_obj.insert("maxItems".to_string(), json!(max_items));
1157 }
1158 } else if let Some(items_schema) = &obj_schema.items {
1159 let items_json = Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1161 schema_obj.insert("items".to_string(), items_json);
1162 }
1163 }
1164
1165 if let Some(format) = &obj_schema.format {
1167 schema_obj.insert("format".to_string(), json!(format));
1168 }
1169
1170 if let Some(example) = &obj_schema.example {
1171 schema_obj.insert("example".to_string(), example.clone());
1172 }
1173
1174 if !obj_schema.examples.is_empty() {
1177 schema_obj.insert("examples".to_string(), json!(&obj_schema.examples));
1178 }
1179
1180 if let Some(default) = &obj_schema.default {
1181 schema_obj.insert("default".to_string(), default.clone());
1182 }
1183
1184 if !obj_schema.enum_values.is_empty() {
1185 schema_obj.insert("enum".to_string(), json!(&obj_schema.enum_values));
1186 }
1187
1188 if let Some(min) = &obj_schema.minimum {
1189 schema_obj.insert("minimum".to_string(), json!(min));
1190 }
1191
1192 if let Some(max) = &obj_schema.maximum {
1193 schema_obj.insert("maximum".to_string(), json!(max));
1194 }
1195
1196 if let Some(min_length) = &obj_schema.min_length {
1197 schema_obj.insert("minLength".to_string(), json!(min_length));
1198 }
1199
1200 if let Some(max_length) = &obj_schema.max_length {
1201 schema_obj.insert("maxLength".to_string(), json!(max_length));
1202 }
1203
1204 if let Some(pattern) = &obj_schema.pattern {
1205 schema_obj.insert("pattern".to_string(), json!(pattern));
1206 }
1207
1208 Ok(Value::Object(schema_obj))
1209 }
1210
1211 fn convert_member_schema(
1221 schema_ref: &ObjectOrReference<ObjectSchema>,
1222 spec: &Spec,
1223 visited: &mut HashSet<String>,
1224 ) -> Result<Value, Error> {
1225 let snapshot = visited.clone();
1226 let result = match schema_ref {
1227 ObjectOrReference::Object(schema) => {
1228 Self::convert_object_schema_to_json_schema(schema, spec, visited)
1229 }
1230 ObjectOrReference::Ref { ref_path, .. } => {
1231 Self::resolve_reference(ref_path, spec, visited).and_then(|resolved| {
1232 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)
1233 })
1234 }
1235 };
1236 *visited = snapshot;
1237 result
1238 }
1239
1240 fn merge_json_schema(dst: &mut serde_json::Map<String, Value>, src: Value) {
1250 let Value::Object(src) = src else {
1251 return;
1252 };
1253 for (key, value) in src {
1254 match key.as_str() {
1255 "properties" => {
1256 let entry = dst
1257 .entry("properties")
1258 .or_insert_with(|| Value::Object(serde_json::Map::new()));
1259 if let (Some(dst_props), Value::Object(src_props)) =
1260 (entry.as_object_mut(), value)
1261 {
1262 for (prop, schema) in src_props {
1263 dst_props.entry(prop).or_insert(schema);
1264 }
1265 }
1266 }
1267 "required" => {
1268 let entry = dst
1269 .entry("required")
1270 .or_insert_with(|| Value::Array(vec![]));
1271 if let (Some(dst_required), Value::Array(src_required)) =
1272 (entry.as_array_mut(), value)
1273 {
1274 for item in src_required {
1275 if !dst_required.contains(&item) {
1276 dst_required.push(item);
1277 }
1278 }
1279 }
1280 }
1281 "additionalProperties" => {
1282 let restrictive = dst.get("additionalProperties") == Some(&Value::Bool(false))
1283 || value == Value::Bool(false);
1284 if restrictive {
1285 dst.insert("additionalProperties".to_string(), Value::Bool(false));
1286 } else {
1287 dst.entry("additionalProperties").or_insert(value);
1288 }
1289 }
1290 "type" => match dst.get("type") {
1291 None => {
1292 dst.insert("type".to_string(), value);
1293 }
1294 Some(Value::String(existing))
1295 if existing != "object" && value == Value::String("object".to_string()) =>
1296 {
1297 dst.insert("type".to_string(), value);
1298 }
1299 _ => {}
1300 },
1301 _ => {
1302 dst.entry(key).or_insert(value);
1303 }
1304 }
1305 }
1306 }
1307
1308 fn schema_type_to_string(schema_type: &SchemaType) -> String {
1310 match schema_type {
1311 SchemaType::Boolean => "boolean",
1312 SchemaType::Integer => "integer",
1313 SchemaType::Number => "number",
1314 SchemaType::String => "string",
1315 SchemaType::Array => "array",
1316 SchemaType::Object => "object",
1317 SchemaType::Null => "null",
1318 }
1319 .to_string()
1320 }
1321
1322 fn resolve_reference(
1332 ref_path: &str,
1333 spec: &Spec,
1334 visited: &mut HashSet<String>,
1335 ) -> Result<ObjectSchema, Error> {
1336 if visited.contains(ref_path) {
1338 return Err(Error::ToolGeneration(format!(
1339 "Circular reference detected: {ref_path}"
1340 )));
1341 }
1342
1343 visited.insert(ref_path.to_string());
1345
1346 if !ref_path.starts_with("#/components/schemas/") {
1349 return Err(Error::ToolGeneration(format!(
1350 "Unsupported reference format: {ref_path}. Only #/components/schemas/ references are supported"
1351 )));
1352 }
1353
1354 let schema_name = ref_path.strip_prefix("#/components/schemas/").unwrap();
1355
1356 let components = spec.components.as_ref().ok_or_else(|| {
1358 Error::ToolGeneration(format!(
1359 "Reference {ref_path} points to components, but spec has no components section"
1360 ))
1361 })?;
1362
1363 let schema_ref = components.schemas.get(schema_name).ok_or_else(|| {
1364 Error::ToolGeneration(format!(
1365 "Schema '{schema_name}' not found in components/schemas"
1366 ))
1367 })?;
1368
1369 let resolved_schema = match schema_ref {
1371 ObjectOrReference::Object(obj_schema) => obj_schema.clone(),
1372 ObjectOrReference::Ref {
1373 ref_path: nested_ref,
1374 ..
1375 } => {
1376 Self::resolve_reference(nested_ref, spec, visited)?
1378 }
1379 };
1380
1381 Ok(resolved_schema)
1388 }
1389
1390 fn resolve_reference_with_metadata(
1395 ref_path: &str,
1396 summary: Option<String>,
1397 description: Option<String>,
1398 spec: &Spec,
1399 visited: &mut HashSet<String>,
1400 ) -> Result<(ObjectSchema, ReferenceMetadata), Error> {
1401 let resolved_schema = Self::resolve_reference(ref_path, spec, visited)?;
1402 let metadata = ReferenceMetadata::new(summary, description);
1403 Ok((resolved_schema, metadata))
1404 }
1405
1406 fn generate_parameter_schema(
1408 parameters: &[ObjectOrReference<Parameter>],
1409 _method: &str,
1410 request_body: &Option<ObjectOrReference<RequestBody>>,
1411 spec: &Spec,
1412 skip_parameter_descriptions: bool,
1413 parameter_examples_in_description: bool,
1414 ) -> Result<
1415 (
1416 Value,
1417 std::collections::HashMap<String, crate::tool::ParameterMapping>,
1418 ),
1419 Error,
1420 > {
1421 let mut properties = serde_json::Map::new();
1422 let mut required = Vec::new();
1423 let mut parameter_mappings = std::collections::HashMap::new();
1424
1425 let mut path_params = Vec::new();
1427 let mut query_params = Vec::new();
1428 let mut header_params = Vec::new();
1429 let mut cookie_params = Vec::new();
1430
1431 for param_ref in parameters {
1432 let param = match param_ref {
1433 ObjectOrReference::Object(param) => param,
1434 ObjectOrReference::Ref { ref_path, .. } => {
1435 warn!(
1439 reference_path = %ref_path,
1440 "Parameter reference not resolved"
1441 );
1442 continue;
1443 }
1444 };
1445
1446 match ¶m.location {
1447 ParameterIn::Query => query_params.push(param),
1448 ParameterIn::Header => header_params.push(param),
1449 ParameterIn::Path => path_params.push(param),
1450 ParameterIn::Cookie => cookie_params.push(param),
1451 }
1452 }
1453
1454 for param in path_params {
1456 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1457 param,
1458 ParameterIn::Path,
1459 spec,
1460 skip_parameter_descriptions,
1461 parameter_examples_in_description,
1462 )?;
1463
1464 let sanitized_name = sanitize_property_name(¶m.name);
1466 if sanitized_name != param.name {
1467 annotations = annotations.with_original_name(param.name.clone());
1468 }
1469
1470 let explode = annotations
1472 .annotations
1473 .iter()
1474 .find_map(|a| {
1475 if let Annotation::Explode(e) = a {
1476 Some(*e)
1477 } else {
1478 None
1479 }
1480 })
1481 .unwrap_or(true);
1482
1483 parameter_mappings.insert(
1485 sanitized_name.clone(),
1486 crate::tool::ParameterMapping {
1487 sanitized_name: sanitized_name.clone(),
1488 original_name: param.name.clone(),
1489 location: "path".to_string(),
1490 explode,
1491 },
1492 );
1493
1494 properties.insert(sanitized_name.clone(), param_schema);
1496 required.push(sanitized_name);
1497 }
1498
1499 for param in &query_params {
1501 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1502 param,
1503 ParameterIn::Query,
1504 spec,
1505 skip_parameter_descriptions,
1506 parameter_examples_in_description,
1507 )?;
1508
1509 let sanitized_name = sanitize_property_name(¶m.name);
1511 if sanitized_name != param.name {
1512 annotations = annotations.with_original_name(param.name.clone());
1513 }
1514
1515 let explode = annotations
1517 .annotations
1518 .iter()
1519 .find_map(|a| {
1520 if let Annotation::Explode(e) = a {
1521 Some(*e)
1522 } else {
1523 None
1524 }
1525 })
1526 .unwrap_or(true);
1527
1528 parameter_mappings.insert(
1530 sanitized_name.clone(),
1531 crate::tool::ParameterMapping {
1532 sanitized_name: sanitized_name.clone(),
1533 original_name: param.name.clone(),
1534 location: "query".to_string(),
1535 explode,
1536 },
1537 );
1538
1539 properties.insert(sanitized_name.clone(), param_schema);
1541 if param.required.unwrap_or(false) {
1542 required.push(sanitized_name);
1543 }
1544 }
1545
1546 for param in &header_params {
1548 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1549 param,
1550 ParameterIn::Header,
1551 spec,
1552 skip_parameter_descriptions,
1553 parameter_examples_in_description,
1554 )?;
1555
1556 let prefixed_name = format!("header_{}", param.name);
1558 let sanitized_name = sanitize_property_name(&prefixed_name);
1559 if sanitized_name != prefixed_name {
1560 annotations = annotations.with_original_name(param.name.clone());
1561 }
1562
1563 let explode = annotations
1565 .annotations
1566 .iter()
1567 .find_map(|a| {
1568 if let Annotation::Explode(e) = a {
1569 Some(*e)
1570 } else {
1571 None
1572 }
1573 })
1574 .unwrap_or(true);
1575
1576 parameter_mappings.insert(
1578 sanitized_name.clone(),
1579 crate::tool::ParameterMapping {
1580 sanitized_name: sanitized_name.clone(),
1581 original_name: param.name.clone(),
1582 location: "header".to_string(),
1583 explode,
1584 },
1585 );
1586
1587 properties.insert(sanitized_name.clone(), param_schema);
1589 if param.required.unwrap_or(false) {
1590 required.push(sanitized_name);
1591 }
1592 }
1593
1594 for param in &cookie_params {
1596 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1597 param,
1598 ParameterIn::Cookie,
1599 spec,
1600 skip_parameter_descriptions,
1601 parameter_examples_in_description,
1602 )?;
1603
1604 let prefixed_name = format!("cookie_{}", param.name);
1606 let sanitized_name = sanitize_property_name(&prefixed_name);
1607 if sanitized_name != prefixed_name {
1608 annotations = annotations.with_original_name(param.name.clone());
1609 }
1610
1611 let explode = annotations
1613 .annotations
1614 .iter()
1615 .find_map(|a| {
1616 if let Annotation::Explode(e) = a {
1617 Some(*e)
1618 } else {
1619 None
1620 }
1621 })
1622 .unwrap_or(true);
1623
1624 parameter_mappings.insert(
1626 sanitized_name.clone(),
1627 crate::tool::ParameterMapping {
1628 sanitized_name: sanitized_name.clone(),
1629 original_name: param.name.clone(),
1630 location: "cookie".to_string(),
1631 explode,
1632 },
1633 );
1634
1635 properties.insert(sanitized_name.clone(), param_schema);
1637 if param.required.unwrap_or(false) {
1638 required.push(sanitized_name);
1639 }
1640 }
1641
1642 if let Some(request_body) = request_body
1644 && let Some((body_schema, _annotations, is_required)) =
1645 Self::convert_request_body_to_json_schema(request_body, spec)?
1646 {
1647 parameter_mappings.insert(
1649 "request_body".to_string(),
1650 crate::tool::ParameterMapping {
1651 sanitized_name: "request_body".to_string(),
1652 original_name: "request_body".to_string(),
1653 location: "body".to_string(),
1654 explode: false,
1655 },
1656 );
1657
1658 properties.insert("request_body".to_string(), body_schema);
1660 if is_required {
1661 required.push("request_body".to_string());
1662 }
1663 }
1664
1665 if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
1667 properties.insert(
1669 "timeout_seconds".to_string(),
1670 json!({
1671 "type": "integer",
1672 "description": "Request timeout in seconds",
1673 "minimum": 1,
1674 "maximum": 300,
1675 "default": 30
1676 }),
1677 );
1678 }
1679
1680 let schema = json!({
1681 "type": "object",
1682 "properties": properties,
1683 "required": required,
1684 "additionalProperties": false
1685 });
1686
1687 Ok((schema, parameter_mappings))
1688 }
1689
1690 fn convert_parameter_schema(
1692 param: &Parameter,
1693 location: ParameterIn,
1694 spec: &Spec,
1695 skip_parameter_descriptions: bool,
1696 parameter_examples_in_description: bool,
1697 ) -> Result<(Value, Annotations), Error> {
1698 let base_schema = if let Some(schema_ref) = ¶m.schema {
1700 match schema_ref {
1701 ObjectOrReference::Object(obj_schema) => {
1702 let mut visited = HashSet::new();
1703 Self::convert_schema_to_json_schema(
1704 &Schema::Object(Box::new(ObjectOrReference::Object(obj_schema.clone()))),
1705 spec,
1706 &mut visited,
1707 )?
1708 }
1709 ObjectOrReference::Ref {
1710 ref_path,
1711 summary,
1712 description,
1713 } => {
1714 let mut visited = HashSet::new();
1716 match Self::resolve_reference_with_metadata(
1717 ref_path,
1718 summary.clone(),
1719 description.clone(),
1720 spec,
1721 &mut visited,
1722 ) {
1723 Ok((resolved_schema, ref_metadata)) => {
1724 let mut schema_json = Self::convert_schema_to_json_schema(
1725 &Schema::Object(Box::new(ObjectOrReference::Object(
1726 resolved_schema,
1727 ))),
1728 spec,
1729 &mut visited,
1730 )?;
1731
1732 if let Value::Object(ref mut schema_obj) = schema_json {
1734 if let Some(ref_desc) = ref_metadata.best_description() {
1736 schema_obj.insert("description".to_string(), json!(ref_desc));
1737 }
1738 }
1741
1742 schema_json
1743 }
1744 Err(_) => {
1745 json!({"type": "string"})
1747 }
1748 }
1749 }
1750 }
1751 } else {
1752 json!({"type": "string"})
1754 };
1755
1756 let mut result = match base_schema {
1758 Value::Object(obj) => obj,
1759 _ => {
1760 return Err(Error::ToolGeneration(format!(
1762 "Internal error: schema converter returned non-object for parameter '{}'",
1763 param.name
1764 )));
1765 }
1766 };
1767
1768 let mut collected_examples: Vec<Value> = Vec::new();
1771
1772 if let Some(example) = ¶m.example {
1774 collected_examples.push(example.clone());
1775 }
1776 for example_ref in param.examples.values() {
1778 if let ObjectOrReference::Object(example_obj) = example_ref
1779 && let Some(value) = &example_obj.value
1780 {
1781 collected_examples.push(value.clone());
1782 }
1783 }
1785 if let Some(example) = result.get("example") {
1787 collected_examples.push(example.clone());
1788 }
1789 if let Some(Value::Array(examples)) = result.get("examples") {
1791 collected_examples.extend(examples.iter().cloned());
1792 }
1793 let lifted_item_examples: Option<Vec<Value>> = (result.get("type")
1798 == Some(&json!("array")))
1799 .then(|| {
1800 result
1801 .get("items")
1802 .and_then(|items| items.get("examples"))
1803 .and_then(Value::as_array)
1804 .cloned()
1805 })
1806 .flatten();
1807 if let Some(item_examples) = lifted_item_examples {
1808 for item_example in &item_examples {
1809 collected_examples.push(json!([item_example]));
1810 }
1811 if let Some(Value::Object(items)) = result.get_mut("items") {
1812 items.remove("examples");
1813 }
1814 }
1815 let mut deduped: Vec<Value> = Vec::with_capacity(collected_examples.len());
1817 for example in collected_examples {
1818 if !deduped.contains(&example) {
1819 deduped.push(example);
1820 }
1821 }
1822 let collected_examples = deduped;
1823
1824 result.remove("example");
1831 result.remove("examples");
1832
1833 let base_description = param
1834 .description
1835 .as_ref()
1836 .map(|d| d.to_string())
1837 .or_else(|| {
1838 result
1839 .get("description")
1840 .and_then(|d| d.as_str())
1841 .map(|d| d.to_string())
1842 })
1843 .unwrap_or_else(|| format!("{} parameter", param.name));
1844
1845 let description = if parameter_examples_in_description {
1846 match Self::format_examples_for_description(&collected_examples) {
1847 Some(examples_str) => format!("{base_description}. {examples_str}"),
1848 None => base_description,
1849 }
1850 } else {
1851 base_description
1852 };
1853
1854 if !skip_parameter_descriptions {
1855 result.insert("description".to_string(), json!(description));
1856 }
1857
1858 if !parameter_examples_in_description && !collected_examples.is_empty() {
1859 result.insert("examples".to_string(), json!(collected_examples));
1860 }
1861
1862 let mut annotations = Annotations::new()
1864 .with_location(Location::Parameter(location))
1865 .with_required(param.required.unwrap_or(false));
1866
1867 if let Some(explode) = param.explode {
1869 annotations = annotations.with_explode(explode);
1870 } else {
1871 let default_explode = match ¶m.style {
1875 Some(ParameterStyle::Form) | None => true, _ => false,
1877 };
1878 annotations = annotations.with_explode(default_explode);
1879 }
1880
1881 Ok((Value::Object(result), annotations))
1882 }
1883
1884 fn format_examples_for_description(examples: &[Value]) -> Option<String> {
1886 if examples.is_empty() {
1887 return None;
1888 }
1889
1890 if examples.len() == 1 {
1891 let example_str =
1892 serde_json::to_string(&examples[0]).unwrap_or_else(|_| "null".to_string());
1893 Some(format!("Example: `{example_str}`"))
1894 } else {
1895 let mut result = String::from("Examples:\n");
1896 for ex in examples {
1897 let json_str = serde_json::to_string(ex).unwrap_or_else(|_| "null".to_string());
1898 result.push_str(&format!("- `{json_str}`\n"));
1899 }
1900 result.pop();
1902 Some(result)
1903 }
1904 }
1905
1906 fn convert_prefix_items_to_draft07(
1917 prefix_items: &[ObjectOrReference<ObjectSchema>],
1918 items: &Option<Box<Schema>>,
1919 result: &mut serde_json::Map<String, Value>,
1920 spec: &Spec,
1921 ) -> Result<(), Error> {
1922 let prefix_count = prefix_items.len();
1923
1924 let mut item_types = Vec::new();
1926 for prefix_item in prefix_items {
1927 match prefix_item {
1928 ObjectOrReference::Object(obj_schema) => {
1929 if let Some(schema_type) = &obj_schema.schema_type {
1930 match schema_type {
1931 SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
1932 SchemaTypeSet::Single(SchemaType::Integer) => {
1933 item_types.push("integer")
1934 }
1935 SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
1936 SchemaTypeSet::Single(SchemaType::Boolean) => {
1937 item_types.push("boolean")
1938 }
1939 SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
1940 SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
1941 _ => item_types.push("string"), }
1943 } else {
1944 item_types.push("string"); }
1946 }
1947 ObjectOrReference::Ref { ref_path, .. } => {
1948 let mut visited = HashSet::new();
1950 match Self::resolve_reference(ref_path, spec, &mut visited) {
1951 Ok(resolved_schema) => {
1952 if let Some(schema_type_set) = &resolved_schema.schema_type {
1954 match schema_type_set {
1955 SchemaTypeSet::Single(SchemaType::String) => {
1956 item_types.push("string")
1957 }
1958 SchemaTypeSet::Single(SchemaType::Integer) => {
1959 item_types.push("integer")
1960 }
1961 SchemaTypeSet::Single(SchemaType::Number) => {
1962 item_types.push("number")
1963 }
1964 SchemaTypeSet::Single(SchemaType::Boolean) => {
1965 item_types.push("boolean")
1966 }
1967 SchemaTypeSet::Single(SchemaType::Array) => {
1968 item_types.push("array")
1969 }
1970 SchemaTypeSet::Single(SchemaType::Object) => {
1971 item_types.push("object")
1972 }
1973 _ => item_types.push("string"), }
1975 } else {
1976 item_types.push("string"); }
1978 }
1979 Err(_) => {
1980 item_types.push("string");
1982 }
1983 }
1984 }
1985 }
1986 }
1987
1988 let items_is_false =
1990 matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
1991
1992 if items_is_false {
1993 result.insert("minItems".to_string(), json!(prefix_count));
1995 result.insert("maxItems".to_string(), json!(prefix_count));
1996 }
1997
1998 let unique_types: std::collections::BTreeSet<_> = item_types.into_iter().collect();
2000
2001 if unique_types.len() == 1 {
2002 let item_type = unique_types.into_iter().next().unwrap();
2004 result.insert("items".to_string(), json!({"type": item_type}));
2005 } else if unique_types.len() > 1 {
2006 let one_of: Vec<Value> = unique_types
2008 .into_iter()
2009 .map(|t| json!({"type": t}))
2010 .collect();
2011 result.insert("items".to_string(), json!({"oneOf": one_of}));
2012 }
2013
2014 Ok(())
2015 }
2016
2017 fn convert_request_body_to_json_schema(
2029 request_body_ref: &ObjectOrReference<RequestBody>,
2030 spec: &Spec,
2031 ) -> Result<Option<(Value, Annotations, bool)>, Error> {
2032 match request_body_ref {
2033 ObjectOrReference::Object(request_body) => {
2034 if let Some(media_type) = request_body.content.get("multipart/form-data") {
2036 return Self::convert_multipart_request_body(request_body, media_type, spec);
2037 }
2038
2039 let schema_info = request_body
2042 .content
2043 .get(mime::APPLICATION_JSON.as_ref())
2044 .or_else(|| request_body.content.get("application/json"))
2045 .or_else(|| {
2046 request_body.content.values().next()
2048 });
2049
2050 if let Some(media_type) = schema_info {
2051 if let Some(schema_ref) = &media_type.schema {
2052 let schema = Schema::Object(Box::new(schema_ref.clone()));
2054
2055 let mut visited = HashSet::new();
2057 let converted_schema =
2058 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?;
2059
2060 let mut schema_obj = match converted_schema {
2062 Value::Object(obj) => obj,
2063 _ => {
2064 let mut obj = serde_json::Map::new();
2066 obj.insert("type".to_string(), json!("object"));
2067 obj.insert("additionalProperties".to_string(), json!(true));
2068 obj
2069 }
2070 };
2071
2072 if !schema_obj.contains_key("description") {
2074 let description = request_body
2075 .description
2076 .clone()
2077 .unwrap_or_else(|| "Request body data".to_string());
2078 schema_obj.insert("description".to_string(), json!(description));
2079 }
2080
2081 let annotations = Annotations::new()
2083 .with_location(Location::Body)
2084 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
2085
2086 let required = request_body.required.unwrap_or(false);
2087 Ok(Some((Value::Object(schema_obj), annotations, required)))
2088 } else {
2089 Ok(None)
2090 }
2091 } else {
2092 Ok(None)
2093 }
2094 }
2095 ObjectOrReference::Ref {
2096 ref_path: _,
2097 summary,
2098 description,
2099 } => {
2100 let ref_metadata = ReferenceMetadata::new(summary.clone(), description.clone());
2102 let enhanced_description = ref_metadata
2103 .best_description()
2104 .map(|desc| desc.to_string())
2105 .unwrap_or_else(|| "Request body data".to_string());
2106
2107 let mut result = serde_json::Map::new();
2108 result.insert("type".to_string(), json!("object"));
2109 result.insert("additionalProperties".to_string(), json!(true));
2110 result.insert("description".to_string(), json!(enhanced_description));
2111
2112 let annotations = Annotations::new()
2114 .with_location(Location::Body)
2115 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
2116
2117 Ok(Some((Value::Object(result), annotations, false)))
2118 }
2119 }
2120 }
2121
2122 fn convert_multipart_request_body(
2131 request_body: &RequestBody,
2132 media_type: &oas3::spec::MediaType,
2133 spec: &Spec,
2134 ) -> Result<Option<(Value, Annotations, bool)>, Error> {
2135 let Some(schema_ref) = &media_type.schema else {
2136 return Ok(None);
2137 };
2138
2139 let obj_schema = match schema_ref {
2141 ObjectOrReference::Object(obj) => obj.clone(),
2142 ObjectOrReference::Ref { ref_path, .. } => {
2143 let mut visited = HashSet::new();
2145 Self::resolve_reference(ref_path, spec, &mut visited)?
2146 }
2147 };
2148
2149 let mut props_map = serde_json::Map::new();
2151 let mut file_fields = Vec::new();
2152
2153 for (prop_name, prop_schema_or_ref) in &obj_schema.properties {
2154 let sanitized_name = sanitize_property_name(prop_name);
2155
2156 let prop_schema = if Self::is_file_field_property(prop_schema_or_ref) {
2157 file_fields.push(sanitized_name.clone());
2159
2160 let description = match prop_schema_or_ref {
2162 ObjectOrReference::Object(obj) => obj.description.as_deref(),
2163 ObjectOrReference::Ref { .. } => None,
2164 };
2165
2166 Self::convert_file_field_to_schema(description)
2168 } else {
2169 let schema = Schema::Object(Box::new(prop_schema_or_ref.clone()));
2171 let mut visited = HashSet::new();
2172 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?
2173 };
2174
2175 props_map.insert(sanitized_name, prop_schema);
2176 }
2177
2178 let mut schema_obj = serde_json::Map::new();
2180 schema_obj.insert("type".to_string(), json!("object"));
2181
2182 if !props_map.is_empty() {
2183 schema_obj.insert("properties".to_string(), Value::Object(props_map));
2184 }
2185
2186 if !obj_schema.required.is_empty() {
2188 let sanitized_required: Vec<String> = obj_schema
2190 .required
2191 .iter()
2192 .map(|name| sanitize_property_name(name))
2193 .collect();
2194 schema_obj.insert("required".to_string(), json!(sanitized_required));
2195 }
2196
2197 let description = obj_schema
2199 .description
2200 .clone()
2201 .or_else(|| request_body.description.clone())
2202 .unwrap_or_else(|| "Request body data".to_string());
2203 schema_obj.insert("description".to_string(), json!(description));
2204
2205 let mut annotations = Annotations::new()
2207 .with_location(Location::Body)
2208 .with_content_type("multipart/form-data".to_string());
2209
2210 if !file_fields.is_empty() {
2211 annotations = annotations.with_file_fields(file_fields);
2212 }
2213
2214 let required = request_body.required.unwrap_or(false);
2215 Ok(Some((Value::Object(schema_obj), annotations, required)))
2216 }
2217
2218 pub fn extract_parameters(
2224 tool_metadata: &ToolMetadata,
2225 arguments: &Value,
2226 ) -> Result<ExtractedParameters, ToolCallValidationError> {
2227 let args = arguments.as_object().ok_or_else(|| {
2228 ToolCallValidationError::RequestConstructionError {
2229 reason: "Arguments must be an object".to_string(),
2230 }
2231 })?;
2232
2233 trace!(
2234 tool_name = %tool_metadata.name,
2235 raw_arguments = ?arguments,
2236 "Starting parameter extraction"
2237 );
2238
2239 let mut path_params = HashMap::new();
2240 let mut query_params = HashMap::new();
2241 let mut header_params = HashMap::new();
2242 let mut cookie_params = HashMap::new();
2243 let mut body_params = HashMap::new();
2244 let mut config = RequestConfig::default();
2245
2246 if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
2248 config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
2249 }
2250
2251 for (key, value) in args {
2253 if key == "timeout_seconds" {
2254 continue; }
2256
2257 if key == "request_body" {
2259 body_params.insert("request_body".to_string(), value.clone());
2260 continue;
2261 }
2262
2263 let mapping = tool_metadata.parameter_mappings.get(key);
2265
2266 if let Some(mapping) = mapping {
2267 match mapping.location.as_str() {
2269 "path" => {
2270 path_params.insert(mapping.original_name.clone(), value.clone());
2271 }
2272 "query" => {
2273 query_params.insert(
2274 mapping.original_name.clone(),
2275 QueryParameter::new(value.clone(), mapping.explode),
2276 );
2277 }
2278 "header" => {
2279 header_params.insert(mapping.original_name.clone(), value.clone());
2280 }
2281 "cookie" => {
2282 cookie_params.insert(mapping.original_name.clone(), value.clone());
2283 }
2284 "body" => {
2285 body_params.insert(mapping.original_name.clone(), value.clone());
2286 }
2287 _ => {
2288 return Err(ToolCallValidationError::RequestConstructionError {
2289 reason: format!("Unknown parameter location for parameter: {key}"),
2290 });
2291 }
2292 }
2293 } else {
2294 let location = Self::get_parameter_location(tool_metadata, key).map_err(|e| {
2296 ToolCallValidationError::RequestConstructionError {
2297 reason: e.to_string(),
2298 }
2299 })?;
2300
2301 let original_name = Self::get_original_parameter_name(tool_metadata, key);
2302
2303 match location.as_str() {
2304 "path" => {
2305 path_params
2306 .insert(original_name.unwrap_or_else(|| key.clone()), value.clone());
2307 }
2308 "query" => {
2309 let param_name = original_name.unwrap_or_else(|| key.clone());
2310 let explode = Self::get_parameter_explode(tool_metadata, key);
2311 query_params
2312 .insert(param_name, QueryParameter::new(value.clone(), explode));
2313 }
2314 "header" => {
2315 let header_name = if let Some(orig) = original_name {
2316 orig
2317 } else if key.starts_with("header_") {
2318 key.strip_prefix("header_").unwrap_or(key).to_string()
2319 } else {
2320 key.clone()
2321 };
2322 header_params.insert(header_name, value.clone());
2323 }
2324 "cookie" => {
2325 let cookie_name = if let Some(orig) = original_name {
2326 orig
2327 } else if key.starts_with("cookie_") {
2328 key.strip_prefix("cookie_").unwrap_or(key).to_string()
2329 } else {
2330 key.clone()
2331 };
2332 cookie_params.insert(cookie_name, value.clone());
2333 }
2334 "body" => {
2335 let body_name = if key.starts_with("body_") {
2336 key.strip_prefix("body_").unwrap_or(key).to_string()
2337 } else {
2338 key.clone()
2339 };
2340 body_params.insert(body_name, value.clone());
2341 }
2342 _ => {
2343 return Err(ToolCallValidationError::RequestConstructionError {
2344 reason: format!("Unknown parameter location for parameter: {key}"),
2345 });
2346 }
2347 }
2348 }
2349 }
2350
2351 let extracted = ExtractedParameters {
2352 path: path_params,
2353 query: query_params,
2354 headers: header_params,
2355 cookies: cookie_params,
2356 body: body_params,
2357 config,
2358 };
2359
2360 trace!(
2361 tool_name = %tool_metadata.name,
2362 extracted_parameters = ?extracted,
2363 "Parameter extraction completed"
2364 );
2365
2366 Self::validate_parameters(tool_metadata, arguments)?;
2368
2369 Ok(extracted)
2370 }
2371
2372 fn get_original_parameter_name(
2374 tool_metadata: &ToolMetadata,
2375 param_name: &str,
2376 ) -> Option<String> {
2377 tool_metadata
2378 .parameters
2379 .get("properties")
2380 .and_then(|p| p.as_object())
2381 .and_then(|props| props.get(param_name))
2382 .and_then(|schema| schema.get(X_ORIGINAL_NAME))
2383 .and_then(|v| v.as_str())
2384 .map(|s| s.to_string())
2385 }
2386
2387 fn get_parameter_explode(tool_metadata: &ToolMetadata, param_name: &str) -> bool {
2389 tool_metadata
2390 .parameters
2391 .get("properties")
2392 .and_then(|p| p.as_object())
2393 .and_then(|props| props.get(param_name))
2394 .and_then(|schema| schema.get(X_PARAMETER_EXPLODE))
2395 .and_then(|v| v.as_bool())
2396 .unwrap_or(true) }
2398
2399 fn get_parameter_location(
2401 tool_metadata: &ToolMetadata,
2402 param_name: &str,
2403 ) -> Result<String, Error> {
2404 let properties = tool_metadata
2405 .parameters
2406 .get("properties")
2407 .and_then(|p| p.as_object())
2408 .ok_or_else(|| Error::ToolGeneration("Invalid tool parameters schema".to_string()))?;
2409
2410 if let Some(param_schema) = properties.get(param_name)
2411 && let Some(location) = param_schema
2412 .get(X_PARAMETER_LOCATION)
2413 .and_then(|v| v.as_str())
2414 {
2415 return Ok(location.to_string());
2416 }
2417
2418 if param_name.starts_with("header_") {
2420 Ok("header".to_string())
2421 } else if param_name.starts_with("cookie_") {
2422 Ok("cookie".to_string())
2423 } else if param_name.starts_with("body_") {
2424 Ok("body".to_string())
2425 } else {
2426 Ok("query".to_string())
2428 }
2429 }
2430
2431 fn validate_parameters(
2433 tool_metadata: &ToolMetadata,
2434 arguments: &Value,
2435 ) -> Result<(), ToolCallValidationError> {
2436 let schema = &tool_metadata.parameters;
2437
2438 let required_params = schema
2440 .get("required")
2441 .and_then(|r| r.as_array())
2442 .map(|arr| {
2443 arr.iter()
2444 .filter_map(|v| v.as_str())
2445 .collect::<std::collections::HashSet<_>>()
2446 })
2447 .unwrap_or_default();
2448
2449 let properties = schema
2450 .get("properties")
2451 .and_then(|p| p.as_object())
2452 .ok_or_else(|| ToolCallValidationError::RequestConstructionError {
2453 reason: "Tool schema missing properties".to_string(),
2454 })?;
2455
2456 let args = arguments.as_object().ok_or_else(|| {
2457 ToolCallValidationError::RequestConstructionError {
2458 reason: "Arguments must be an object".to_string(),
2459 }
2460 })?;
2461
2462 let mut all_errors = Vec::new();
2464
2465 all_errors.extend(Self::check_unknown_parameters(args, properties));
2467
2468 all_errors.extend(Self::check_missing_required(
2470 args,
2471 properties,
2472 &required_params,
2473 ));
2474
2475 all_errors.extend(Self::validate_parameter_values(
2477 args,
2478 properties,
2479 &required_params,
2480 ));
2481
2482 if !all_errors.is_empty() {
2484 return Err(ToolCallValidationError::InvalidParameters {
2485 violations: all_errors,
2486 });
2487 }
2488
2489 Ok(())
2490 }
2491
2492 fn check_unknown_parameters(
2494 args: &serde_json::Map<String, Value>,
2495 properties: &serde_json::Map<String, Value>,
2496 ) -> Vec<ValidationError> {
2497 let mut errors = Vec::new();
2498
2499 let valid_params: Vec<String> = properties.keys().map(|s| s.to_string()).collect();
2501
2502 for (arg_name, _) in args.iter() {
2504 if !properties.contains_key(arg_name) {
2505 errors.push(ValidationError::invalid_parameter(
2507 arg_name.clone(),
2508 &valid_params,
2509 ));
2510 }
2511 }
2512
2513 errors
2514 }
2515
2516 fn check_missing_required(
2518 args: &serde_json::Map<String, Value>,
2519 properties: &serde_json::Map<String, Value>,
2520 required_params: &HashSet<&str>,
2521 ) -> Vec<ValidationError> {
2522 let mut errors = Vec::new();
2523
2524 for required_param in required_params {
2525 if !args.contains_key(*required_param) {
2526 let param_schema = properties.get(*required_param);
2528
2529 let description = param_schema
2530 .and_then(|schema| schema.get("description"))
2531 .and_then(|d| d.as_str())
2532 .map(|s| s.to_string());
2533
2534 let expected_type = param_schema
2535 .and_then(Self::get_expected_type)
2536 .unwrap_or_else(|| "unknown".to_string());
2537
2538 errors.push(ValidationError::MissingRequiredParameter {
2539 parameter: (*required_param).to_string(),
2540 description,
2541 expected_type,
2542 });
2543 }
2544 }
2545
2546 errors
2547 }
2548
2549 fn validate_parameter_values(
2551 args: &serde_json::Map<String, Value>,
2552 properties: &serde_json::Map<String, Value>,
2553 required_params: &std::collections::HashSet<&str>,
2554 ) -> Vec<ValidationError> {
2555 let mut errors = Vec::new();
2556
2557 for (param_name, param_value) in args {
2558 if let Some(param_schema) = properties.get(param_name) {
2559 let is_null_value = param_value.is_null();
2561 let is_required = required_params.contains(param_name.as_str());
2562
2563 let schema = json!({
2565 "type": "object",
2566 "properties": {
2567 param_name: param_schema
2568 }
2569 });
2570
2571 let compiled = match jsonschema::validator_for(&schema) {
2573 Ok(compiled) => compiled,
2574 Err(e) => {
2575 errors.push(ValidationError::ConstraintViolation {
2576 parameter: param_name.clone(),
2577 message: format!(
2578 "Failed to compile schema for parameter '{param_name}': {e}"
2579 ),
2580 field_path: None,
2581 actual_value: None,
2582 expected_type: None,
2583 constraints: vec![],
2584 });
2585 continue;
2586 }
2587 };
2588
2589 let instance = json!({ param_name: param_value });
2591
2592 let validation_errors: Vec<_> =
2594 compiled.validate(&instance).err().into_iter().collect();
2595
2596 for validation_error in validation_errors {
2597 let error_message = validation_error.to_string();
2599 let instance_path_str = validation_error.instance_path().to_string();
2600 let field_path = if instance_path_str.is_empty() || instance_path_str == "/" {
2601 Some(param_name.clone())
2602 } else {
2603 Some(instance_path_str.trim_start_matches('/').to_string())
2604 };
2605
2606 let constraints = Self::extract_constraints_from_schema(param_schema);
2608
2609 let expected_type = Self::get_expected_type(param_schema);
2611
2612 let maybe_type_error = match &validation_error.kind() {
2616 ValidationErrorKind::Type { kind } => Some(kind),
2617 _ => None,
2618 };
2619 let is_type_error = maybe_type_error.is_some();
2620 let is_null_error = is_null_value
2621 || (is_type_error && validation_error.instance().as_null().is_some());
2622 let message = if is_null_error && let Some(type_error) = maybe_type_error {
2623 let field_name = field_path.as_ref().unwrap_or(param_name);
2625
2626 let final_expected_type =
2628 expected_type.clone().unwrap_or_else(|| match type_error {
2629 TypeKind::Single(json_type) => json_type.to_string(),
2630 TypeKind::Multiple(json_type_set) => json_type_set
2631 .iter()
2632 .map(|t| t.to_string())
2633 .collect::<Vec<_>>()
2634 .join(", "),
2635 });
2636
2637 let actual_field_name = field_path
2640 .as_ref()
2641 .and_then(|path| path.split('/').next_back())
2642 .unwrap_or(param_name);
2643
2644 let is_nested_field = field_path.as_ref().is_some_and(|p| p.contains('/'));
2647
2648 let field_is_required = if is_nested_field {
2649 constraints.iter().any(|c| {
2650 if let ValidationConstraint::Required { properties } = c {
2651 properties.contains(&actual_field_name.to_string())
2652 } else {
2653 false
2654 }
2655 })
2656 } else {
2657 is_required
2658 };
2659
2660 if field_is_required {
2661 format!(
2662 "Parameter '{field_name}' is required and must not be null (expected: {final_expected_type})"
2663 )
2664 } else {
2665 format!(
2666 "Parameter '{field_name}' is optional but must not be null (expected: {final_expected_type})"
2667 )
2668 }
2669 } else {
2670 error_message
2671 };
2672
2673 errors.push(ValidationError::ConstraintViolation {
2674 parameter: param_name.clone(),
2675 message,
2676 field_path,
2677 actual_value: Some(Box::new(param_value.clone())),
2678 expected_type,
2679 constraints,
2680 });
2681 }
2682 }
2683 }
2684
2685 errors
2686 }
2687
2688 fn extract_constraints_from_schema(schema: &Value) -> Vec<ValidationConstraint> {
2690 let mut constraints = Vec::new();
2691
2692 if let Some(min_value) = schema.get("minimum").and_then(|v| v.as_f64()) {
2694 let exclusive = schema
2695 .get("exclusiveMinimum")
2696 .and_then(|v| v.as_bool())
2697 .unwrap_or(false);
2698 constraints.push(ValidationConstraint::Minimum {
2699 value: min_value,
2700 exclusive,
2701 });
2702 }
2703
2704 if let Some(max_value) = schema.get("maximum").and_then(|v| v.as_f64()) {
2706 let exclusive = schema
2707 .get("exclusiveMaximum")
2708 .and_then(|v| v.as_bool())
2709 .unwrap_or(false);
2710 constraints.push(ValidationConstraint::Maximum {
2711 value: max_value,
2712 exclusive,
2713 });
2714 }
2715
2716 if let Some(min_len) = schema
2718 .get("minLength")
2719 .and_then(|v| v.as_u64())
2720 .map(|v| v as usize)
2721 {
2722 constraints.push(ValidationConstraint::MinLength { value: min_len });
2723 }
2724
2725 if let Some(max_len) = schema
2727 .get("maxLength")
2728 .and_then(|v| v.as_u64())
2729 .map(|v| v as usize)
2730 {
2731 constraints.push(ValidationConstraint::MaxLength { value: max_len });
2732 }
2733
2734 if let Some(pattern) = schema
2736 .get("pattern")
2737 .and_then(|v| v.as_str())
2738 .map(|s| s.to_string())
2739 {
2740 constraints.push(ValidationConstraint::Pattern { pattern });
2741 }
2742
2743 if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()).cloned() {
2745 constraints.push(ValidationConstraint::EnumValues {
2746 values: enum_values,
2747 });
2748 }
2749
2750 if let Some(format) = schema
2752 .get("format")
2753 .and_then(|v| v.as_str())
2754 .map(|s| s.to_string())
2755 {
2756 constraints.push(ValidationConstraint::Format { format });
2757 }
2758
2759 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
2761 constraints.push(ValidationConstraint::MultipleOf { value: multiple_of });
2762 }
2763
2764 if let Some(min_items) = schema
2766 .get("minItems")
2767 .and_then(|v| v.as_u64())
2768 .map(|v| v as usize)
2769 {
2770 constraints.push(ValidationConstraint::MinItems { value: min_items });
2771 }
2772
2773 if let Some(max_items) = schema
2775 .get("maxItems")
2776 .and_then(|v| v.as_u64())
2777 .map(|v| v as usize)
2778 {
2779 constraints.push(ValidationConstraint::MaxItems { value: max_items });
2780 }
2781
2782 if let Some(true) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
2784 constraints.push(ValidationConstraint::UniqueItems);
2785 }
2786
2787 if let Some(min_props) = schema
2789 .get("minProperties")
2790 .and_then(|v| v.as_u64())
2791 .map(|v| v as usize)
2792 {
2793 constraints.push(ValidationConstraint::MinProperties { value: min_props });
2794 }
2795
2796 if let Some(max_props) = schema
2798 .get("maxProperties")
2799 .and_then(|v| v.as_u64())
2800 .map(|v| v as usize)
2801 {
2802 constraints.push(ValidationConstraint::MaxProperties { value: max_props });
2803 }
2804
2805 if let Some(const_value) = schema.get("const").cloned() {
2807 constraints.push(ValidationConstraint::ConstValue { value: const_value });
2808 }
2809
2810 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
2812 let properties: Vec<String> = required
2813 .iter()
2814 .filter_map(|v| v.as_str().map(|s| s.to_string()))
2815 .collect();
2816 if !properties.is_empty() {
2817 constraints.push(ValidationConstraint::Required { properties });
2818 }
2819 }
2820
2821 constraints
2822 }
2823
2824 fn get_expected_type(schema: &Value) -> Option<String> {
2826 if let Some(type_value) = schema.get("type") {
2827 if let Some(type_str) = type_value.as_str() {
2828 return Some(type_str.to_string());
2829 } else if let Some(type_array) = type_value.as_array() {
2830 let types: Vec<String> = type_array
2832 .iter()
2833 .filter_map(|v| v.as_str())
2834 .map(|s| s.to_string())
2835 .collect();
2836 if !types.is_empty() {
2837 return Some(types.join(" | "));
2838 }
2839 }
2840 }
2841 None
2842 }
2843
2844 fn wrap_output_schema(
2868 body_schema: &ObjectOrReference<ObjectSchema>,
2869 spec: &Spec,
2870 ) -> Result<Value, Error> {
2871 let mut visited = HashSet::new();
2873 let body_schema_json = match body_schema {
2874 ObjectOrReference::Object(obj_schema) => {
2875 Self::convert_object_schema_to_json_schema(obj_schema, spec, &mut visited)?
2876 }
2877 ObjectOrReference::Ref { ref_path, .. } => {
2878 let resolved = Self::resolve_reference(ref_path, spec, &mut visited)?;
2879 let result =
2880 Self::convert_object_schema_to_json_schema(&resolved, spec, &mut visited)?;
2881 visited.remove(ref_path);
2883 result
2884 }
2885 };
2886
2887 let error_schema = create_error_response_schema();
2888
2889 Ok(json!({
2890 "type": "object",
2891 "description": "Unified response structure with success and error variants",
2892 "required": ["status", "body"],
2893 "additionalProperties": false,
2894 "properties": {
2895 "status": {
2896 "type": "integer",
2897 "description": "HTTP status code",
2898 "minimum": 100,
2899 "maximum": 599
2900 },
2901 "body": {
2902 "description": "Response body - either success data or error information",
2903 "oneOf": [
2904 body_schema_json,
2905 error_schema
2906 ]
2907 }
2908 }
2909 }))
2910 }
2911
2912 #[must_use]
2923 pub fn is_file_field(schema: &Schema) -> bool {
2924 match schema {
2925 Schema::Object(obj_or_ref) => match obj_or_ref.as_ref() {
2926 ObjectOrReference::Object(obj_schema) => {
2927 Self::is_file_field_object_schema(obj_schema)
2928 }
2929 ObjectOrReference::Ref { .. } => {
2930 false
2932 }
2933 },
2934 Schema::Boolean(_) => false,
2935 }
2936 }
2937
2938 fn is_file_field_object_schema(obj_schema: &ObjectSchema) -> bool {
2943 if let Some(format) = &obj_schema.format {
2944 format == "binary" || format == "byte"
2945 } else {
2946 false
2947 }
2948 }
2949
2950 fn is_file_field_property(prop_schema: &ObjectOrReference<ObjectSchema>) -> bool {
2955 match prop_schema {
2956 ObjectOrReference::Object(obj_schema) => Self::is_file_field_object_schema(obj_schema),
2957 ObjectOrReference::Ref { .. } => {
2958 false
2960 }
2961 }
2962 }
2963
2964 fn convert_file_field_to_schema(original_description: Option<&str>) -> Value {
2976 let description = original_description.unwrap_or("File upload");
2977 json!({
2978 "type": "object",
2979 "description": description,
2980 "properties": {
2981 "content": {
2982 "type": "string",
2983 "description": "File content as data URI (e.g., data:image/png;base64,...)"
2984 },
2985 "filename": {
2986 "type": "string",
2987 "description": "Optional filename for the upload"
2988 }
2989 },
2990 "required": ["content"]
2991 })
2992 }
2993}
2994
2995fn create_error_response_schema() -> Value {
2997 let root_schema = schema_for!(ErrorResponse);
2998 let schema_json = serde_json::to_value(root_schema).expect("Valid error schema");
2999
3000 let definitions = schema_json
3002 .get("$defs")
3003 .or_else(|| schema_json.get("definitions"))
3004 .cloned()
3005 .unwrap_or_else(|| json!({}));
3006
3007 let mut result = schema_json.clone();
3009 if let Some(obj) = result.as_object_mut() {
3010 obj.remove("$schema");
3011 obj.remove("$defs");
3012 obj.remove("definitions");
3013 obj.remove("title");
3014 }
3015
3016 inline_refs(&mut result, &definitions);
3018
3019 result
3020}
3021
3022fn inline_refs(schema: &mut Value, definitions: &Value) {
3024 match schema {
3025 Value::Object(obj) => {
3026 if let Some(ref_value) = obj.get("$ref").cloned()
3028 && let Some(ref_str) = ref_value.as_str()
3029 {
3030 let def_name = ref_str
3032 .strip_prefix("#/$defs/")
3033 .or_else(|| ref_str.strip_prefix("#/definitions/"));
3034
3035 if let Some(name) = def_name
3036 && let Some(definition) = definitions.get(name)
3037 {
3038 *schema = definition.clone();
3040 inline_refs(schema, definitions);
3042 return;
3043 }
3044 }
3045
3046 for (_, value) in obj.iter_mut() {
3048 inline_refs(value, definitions);
3049 }
3050 }
3051 Value::Array(arr) => {
3052 for item in arr.iter_mut() {
3054 inline_refs(item, definitions);
3055 }
3056 }
3057 _ => {} }
3059}
3060
3061#[derive(Debug, Clone)]
3063pub struct QueryParameter {
3064 pub value: Value,
3065 pub explode: bool,
3066}
3067
3068impl QueryParameter {
3069 pub fn new(value: Value, explode: bool) -> Self {
3070 Self { value, explode }
3071 }
3072}
3073
3074#[derive(Debug, Clone)]
3076pub struct ExtractedParameters {
3077 pub path: HashMap<String, Value>,
3078 pub query: HashMap<String, QueryParameter>,
3079 pub headers: HashMap<String, Value>,
3080 pub cookies: HashMap<String, Value>,
3081 pub body: HashMap<String, Value>,
3082 pub config: RequestConfig,
3083}
3084
3085#[derive(Debug, Clone)]
3087pub struct RequestConfig {
3088 pub timeout_seconds: u32,
3089 pub content_type: String,
3090}
3091
3092impl Default for RequestConfig {
3093 fn default() -> Self {
3094 Self {
3095 timeout_seconds: 30,
3096 content_type: mime::APPLICATION_JSON.to_string(),
3097 }
3098 }
3099}
3100
3101#[cfg(test)]
3102mod tests {
3103 use super::*;
3104
3105 use insta::assert_json_snapshot;
3106 use oas3::spec::{
3107 BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
3108 Parameter, ParameterIn, RequestBody, Schema, SchemaType, SchemaTypeSet, Spec,
3109 };
3110 use rmcp::model::Tool;
3111 use serde_json::{Value, json};
3112 use std::collections::BTreeMap;
3113
3114 #[test]
3115 fn converter_preserves_schema_level_examples_plural() {
3116 let spec = create_test_spec();
3117 let schema: ObjectSchema = serde_json::from_value(json!({
3118 "type": "string",
3119 "examples": ["a", "a.b", "a.b.c"],
3120 }))
3121 .expect("valid object schema");
3122 let mut visited = std::collections::HashSet::new();
3123 let result =
3124 ToolGenerator::convert_object_schema_to_json_schema(&schema, &spec, &mut visited)
3125 .expect("conversion succeeds");
3126 assert_eq!(result["type"], json!("string"));
3127 assert_eq!(
3128 result["examples"],
3129 json!(["a", "a.b", "a.b.c"]),
3130 "schema-level plural `examples` must be preserved: {result}"
3131 );
3132 }
3133
3134 fn parameter_with_singular_and_named_map_examples() -> Parameter {
3135 serde_json::from_value(json!({
3136 "name": "q",
3137 "in": "query",
3138 "schema": { "type": "string" },
3139 "example": "alpha",
3140 "examples": {
3141 "beta": { "value": "beta" },
3142 "gamma": { "value": "gamma" },
3143 },
3144 }))
3145 .expect("valid parameter")
3146 }
3147
3148 #[test]
3149 fn parameter_examples_default_to_structured_field() {
3150 let spec = create_test_spec();
3151 let param = parameter_with_singular_and_named_map_examples();
3152 let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3155 ¶m,
3156 ParameterIn::Query,
3157 &spec,
3158 false,
3159 false,
3160 )
3161 .expect("conversion succeeds");
3162 let values: Vec<String> = result["examples"]
3163 .as_array()
3164 .expect("structured `examples` present")
3165 .iter()
3166 .filter_map(|value| value.as_str().map(ToString::to_string))
3167 .collect();
3168 assert!(
3169 values.iter().any(|v| v == "alpha")
3170 && values.iter().any(|v| v == "beta")
3171 && values.iter().any(|v| v == "gamma"),
3172 "all sources chained into structured `examples`: {result}"
3173 );
3174 let description = result["description"].as_str().unwrap_or_default();
3175 assert!(
3176 !description.contains("alpha") && !description.contains("beta"),
3177 "examples must not be duplicated into the description by default: {description}"
3178 );
3179 }
3180
3181 #[test]
3182 fn parameter_examples_in_description_when_flag_set() {
3183 let spec = create_test_spec();
3184 let param = parameter_with_singular_and_named_map_examples();
3185 let (result, _annotations) =
3187 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false, true)
3188 .expect("conversion succeeds");
3189 let description = result["description"].as_str().unwrap_or_default();
3190 assert!(
3191 description.contains("alpha")
3192 && description.contains("beta")
3193 && description.contains("gamma"),
3194 "examples folded into description: {description}"
3195 );
3196 assert!(
3197 result.get("examples").is_none(),
3198 "structured `examples` omitted when folding into the description: {result}"
3199 );
3200 }
3201
3202 #[test]
3203 fn array_parameter_lifts_item_examples_to_parameter_level() {
3204 let spec = create_test_spec();
3205 let param: Parameter = serde_json::from_value(json!({
3209 "name": "include",
3210 "in": "query",
3211 "schema": {
3212 "type": "array",
3213 "items": { "type": "string", "examples": ["camera", "mesh.primitives"] },
3214 },
3215 }))
3216 .expect("valid parameter");
3217 let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3218 ¶m,
3219 ParameterIn::Query,
3220 &spec,
3221 false,
3222 false,
3223 )
3224 .expect("conversion succeeds");
3225 assert_eq!(
3228 result["examples"],
3229 json!([["camera"], ["mesh.primitives"]]),
3230 "array element examples must be lifted to parameter-level examples: {result}"
3231 );
3232 }
3233
3234 #[test]
3235 fn lifting_array_item_examples_clears_them_from_items() {
3236 let spec = create_test_spec();
3237 let param: Parameter = serde_json::from_value(json!({
3238 "name": "include",
3239 "in": "query",
3240 "schema": {
3241 "type": "array",
3242 "items": { "type": "string", "examples": ["camera", "mesh.primitives"] },
3243 },
3244 }))
3245 .expect("valid parameter");
3246 let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3247 ¶m,
3248 ParameterIn::Query,
3249 &spec,
3250 false,
3251 false,
3252 )
3253 .expect("conversion succeeds");
3254 assert!(
3257 result["items"].get("examples").is_none(),
3258 "item-level examples must be cleared once lifted to the parameter level: {result}"
3259 );
3260 }
3261
3262 #[test]
3263 fn array_parameter_examples_lift_snapshot() {
3264 let spec = create_test_spec();
3265 let param: Parameter = serde_json::from_value(json!({
3269 "name": "include",
3270 "in": "query",
3271 "description": "Relationship paths to include.",
3272 "schema": {
3273 "type": "array",
3274 "items": {
3275 "type": "string",
3276 "description": "A relationship path: a dot-separated chain of relationship names.",
3277 "examples": ["camera", "mesh.primitives.material", "mesh.primitives.indices"],
3278 },
3279 },
3280 }))
3281 .expect("valid parameter");
3282 let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3283 ¶m,
3284 ParameterIn::Query,
3285 &spec,
3286 false,
3287 false,
3288 )
3289 .expect("conversion succeeds");
3290 assert_json_snapshot!("array_parameter_examples_lift", result);
3291 }
3292
3293 fn create_test_spec() -> Spec {
3295 Spec {
3296 openapi: "3.0.0".to_string(),
3297 info: oas3::spec::Info {
3298 title: "Test API".to_string(),
3299 version: "1.0.0".to_string(),
3300 summary: None,
3301 description: Some("Test API for unit tests".to_string()),
3302 terms_of_service: None,
3303 contact: None,
3304 license: None,
3305 extensions: Default::default(),
3306 },
3307 components: Some(Components {
3308 schemas: BTreeMap::new(),
3309 responses: BTreeMap::new(),
3310 parameters: BTreeMap::new(),
3311 examples: BTreeMap::new(),
3312 request_bodies: BTreeMap::new(),
3313 headers: BTreeMap::new(),
3314 security_schemes: BTreeMap::new(),
3315 links: BTreeMap::new(),
3316 callbacks: BTreeMap::new(),
3317 path_items: BTreeMap::new(),
3318 extensions: Default::default(),
3319 }),
3320 servers: vec![],
3321 paths: None,
3322 external_docs: None,
3323 tags: vec![],
3324 security: vec![],
3325 webhooks: BTreeMap::new(),
3326 extensions: Default::default(),
3327 }
3328 }
3329
3330 fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
3331 let schema_content = std::fs::read_to_string("schema/2025-06-18/schema.json")
3332 .expect("Failed to read MCP schema file");
3333 let full_schema: Value =
3334 serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
3335
3336 let tool_schema = json!({
3338 "$schema": "http://json-schema.org/draft-07/schema#",
3339 "definitions": full_schema.get("definitions"),
3340 "$ref": "#/definitions/Tool"
3341 });
3342
3343 let validator =
3344 jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
3345
3346 let tool = Tool::from(metadata);
3348
3349 let mcp_tool_json = serde_json::to_value(&tool).expect("Failed to serialize Tool to JSON");
3351
3352 let errors: Vec<String> = validator
3354 .iter_errors(&mcp_tool_json)
3355 .map(|e| e.to_string())
3356 .collect();
3357
3358 if !errors.is_empty() {
3359 panic!("Generated tool failed MCP schema validation: {errors:?}");
3360 }
3361 }
3362
3363 #[test]
3364 fn test_error_schema_structure() {
3365 let error_schema = create_error_response_schema();
3366
3367 assert!(error_schema.get("$schema").is_none());
3369 assert!(error_schema.get("definitions").is_none());
3370
3371 assert_json_snapshot!(error_schema);
3373 }
3374
3375 #[test]
3376 fn test_petstore_get_pet_by_id() {
3377 use oas3::spec::Response;
3378
3379 let mut operation = Operation {
3380 operation_id: Some("getPetById".to_string()),
3381 summary: Some("Find pet by ID".to_string()),
3382 description: Some("Returns a single pet".to_string()),
3383 tags: vec![],
3384 external_docs: None,
3385 parameters: vec![],
3386 request_body: None,
3387 responses: Default::default(),
3388 callbacks: Default::default(),
3389 deprecated: Some(false),
3390 security: vec![],
3391 servers: vec![],
3392 extensions: Default::default(),
3393 };
3394
3395 let param = Parameter {
3397 name: "petId".to_string(),
3398 location: ParameterIn::Path,
3399 description: Some("ID of pet to return".to_string()),
3400 required: Some(true),
3401 deprecated: Some(false),
3402 allow_empty_value: Some(false),
3403 style: None,
3404 explode: None,
3405 allow_reserved: Some(false),
3406 schema: Some(ObjectOrReference::Object(ObjectSchema {
3407 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3408 minimum: Some(serde_json::Number::from(1_i64)),
3409 format: Some("int64".to_string()),
3410 ..Default::default()
3411 })),
3412 example: None,
3413 examples: Default::default(),
3414 content: None,
3415 extensions: Default::default(),
3416 };
3417
3418 operation.parameters.push(ObjectOrReference::Object(param));
3419
3420 let mut responses = BTreeMap::new();
3422 let mut content = BTreeMap::new();
3423 content.insert(
3424 "application/json".to_string(),
3425 MediaType {
3426 extensions: Default::default(),
3427 schema: Some(ObjectOrReference::Object(ObjectSchema {
3428 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3429 properties: {
3430 let mut props = BTreeMap::new();
3431 props.insert(
3432 "id".to_string(),
3433 ObjectOrReference::Object(ObjectSchema {
3434 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3435 format: Some("int64".to_string()),
3436 ..Default::default()
3437 }),
3438 );
3439 props.insert(
3440 "name".to_string(),
3441 ObjectOrReference::Object(ObjectSchema {
3442 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3443 ..Default::default()
3444 }),
3445 );
3446 props.insert(
3447 "status".to_string(),
3448 ObjectOrReference::Object(ObjectSchema {
3449 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3450 ..Default::default()
3451 }),
3452 );
3453 props
3454 },
3455 required: vec!["id".to_string(), "name".to_string()],
3456 ..Default::default()
3457 })),
3458 examples: None,
3459 encoding: Default::default(),
3460 },
3461 );
3462
3463 responses.insert(
3464 "200".to_string(),
3465 ObjectOrReference::Object(Response {
3466 description: Some("successful operation".to_string()),
3467 headers: Default::default(),
3468 content,
3469 links: Default::default(),
3470 extensions: Default::default(),
3471 }),
3472 );
3473 operation.responses = Some(responses);
3474
3475 let spec = create_test_spec();
3476 let metadata = ToolGenerator::generate_tool_metadata(
3477 &operation,
3478 "get".to_string(),
3479 "/pet/{petId}".to_string(),
3480 &spec,
3481 false,
3482 false,
3483 false,
3484 )
3485 .unwrap();
3486
3487 assert_eq!(metadata.name, "getPetById");
3488 assert_eq!(metadata.method, "get");
3489 assert_eq!(metadata.path, "/pet/{petId}");
3490 assert!(
3491 metadata
3492 .description
3493 .clone()
3494 .unwrap()
3495 .contains("Find pet by ID")
3496 );
3497
3498 assert!(metadata.output_schema.is_some());
3500 let output_schema = metadata.output_schema.as_ref().unwrap();
3501
3502 insta::assert_json_snapshot!("test_petstore_get_pet_by_id_output_schema", output_schema);
3504
3505 validate_tool_against_mcp_schema(&metadata);
3507 }
3508
3509 #[test]
3510 fn test_convert_prefix_items_to_draft07_mixed_types() {
3511 let prefix_items = vec![
3514 ObjectOrReference::Object(ObjectSchema {
3515 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3516 format: Some("int32".to_string()),
3517 ..Default::default()
3518 }),
3519 ObjectOrReference::Object(ObjectSchema {
3520 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3521 ..Default::default()
3522 }),
3523 ];
3524
3525 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3527
3528 let mut result = serde_json::Map::new();
3529 let spec = create_test_spec();
3530 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3531 .unwrap();
3532
3533 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_mixed_types", result);
3535 }
3536
3537 #[test]
3538 fn test_convert_prefix_items_to_draft07_uniform_types() {
3539 let prefix_items = vec![
3541 ObjectOrReference::Object(ObjectSchema {
3542 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3543 ..Default::default()
3544 }),
3545 ObjectOrReference::Object(ObjectSchema {
3546 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3547 ..Default::default()
3548 }),
3549 ];
3550
3551 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3553
3554 let mut result = serde_json::Map::new();
3555 let spec = create_test_spec();
3556 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3557 .unwrap();
3558
3559 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_uniform_types", result);
3561 }
3562
3563 #[test]
3564 fn test_array_with_prefix_items_integration() {
3565 let param = Parameter {
3567 name: "coordinates".to_string(),
3568 location: ParameterIn::Query,
3569 description: Some("X,Y coordinates as tuple".to_string()),
3570 required: Some(true),
3571 deprecated: Some(false),
3572 allow_empty_value: Some(false),
3573 style: None,
3574 explode: None,
3575 allow_reserved: Some(false),
3576 schema: Some(ObjectOrReference::Object(ObjectSchema {
3577 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3578 prefix_items: vec![
3579 ObjectOrReference::Object(ObjectSchema {
3580 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3581 format: Some("double".to_string()),
3582 ..Default::default()
3583 }),
3584 ObjectOrReference::Object(ObjectSchema {
3585 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3586 format: Some("double".to_string()),
3587 ..Default::default()
3588 }),
3589 ],
3590 items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
3591 ..Default::default()
3592 })),
3593 example: None,
3594 examples: Default::default(),
3595 content: None,
3596 extensions: Default::default(),
3597 };
3598
3599 let spec = create_test_spec();
3600 let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3601 ¶m,
3602 ParameterIn::Query,
3603 &spec,
3604 false,
3605 false,
3606 )
3607 .unwrap();
3608
3609 insta::assert_json_snapshot!("test_array_with_prefix_items_integration", result);
3611 }
3612
3613 #[test]
3614 fn test_skip_tool_description() {
3615 let operation = Operation {
3616 operation_id: Some("getPetById".to_string()),
3617 summary: Some("Find pet by ID".to_string()),
3618 description: Some("Returns a single pet".to_string()),
3619 tags: vec![],
3620 external_docs: None,
3621 parameters: vec![],
3622 request_body: None,
3623 responses: Default::default(),
3624 callbacks: Default::default(),
3625 deprecated: Some(false),
3626 security: vec![],
3627 servers: vec![],
3628 extensions: Default::default(),
3629 };
3630
3631 let spec = create_test_spec();
3632 let metadata = ToolGenerator::generate_tool_metadata(
3633 &operation,
3634 "get".to_string(),
3635 "/pet/{petId}".to_string(),
3636 &spec,
3637 true,
3638 false,
3639 false,
3640 )
3641 .unwrap();
3642
3643 assert_eq!(metadata.name, "getPetById");
3644 assert_eq!(metadata.method, "get");
3645 assert_eq!(metadata.path, "/pet/{petId}");
3646 assert!(metadata.description.is_none());
3647
3648 insta::assert_json_snapshot!("test_skip_tool_description", metadata);
3650
3651 validate_tool_against_mcp_schema(&metadata);
3653 }
3654
3655 #[test]
3656 fn test_keep_tool_description() {
3657 let description = Some("Returns a single pet".to_string());
3658 let operation = Operation {
3659 operation_id: Some("getPetById".to_string()),
3660 summary: Some("Find pet by ID".to_string()),
3661 description: description.clone(),
3662 tags: vec![],
3663 external_docs: None,
3664 parameters: vec![],
3665 request_body: None,
3666 responses: Default::default(),
3667 callbacks: Default::default(),
3668 deprecated: Some(false),
3669 security: vec![],
3670 servers: vec![],
3671 extensions: Default::default(),
3672 };
3673
3674 let spec = create_test_spec();
3675 let metadata = ToolGenerator::generate_tool_metadata(
3676 &operation,
3677 "get".to_string(),
3678 "/pet/{petId}".to_string(),
3679 &spec,
3680 false,
3681 false,
3682 false,
3683 )
3684 .unwrap();
3685
3686 assert_eq!(metadata.name, "getPetById");
3687 assert_eq!(metadata.method, "get");
3688 assert_eq!(metadata.path, "/pet/{petId}");
3689 assert!(metadata.description.is_some());
3690
3691 insta::assert_json_snapshot!("test_keep_tool_description", metadata);
3693
3694 validate_tool_against_mcp_schema(&metadata);
3696 }
3697
3698 #[test]
3699 fn test_skip_parameter_descriptions() {
3700 let param = Parameter {
3701 name: "status".to_string(),
3702 location: ParameterIn::Query,
3703 description: Some("Filter by status".to_string()),
3704 required: Some(false),
3705 deprecated: Some(false),
3706 allow_empty_value: Some(false),
3707 style: None,
3708 explode: None,
3709 allow_reserved: Some(false),
3710 schema: Some(ObjectOrReference::Object(ObjectSchema {
3711 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3712 enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3713 ..Default::default()
3714 })),
3715 example: Some(json!("available")),
3716 examples: Default::default(),
3717 content: None,
3718 extensions: Default::default(),
3719 };
3720
3721 let spec = create_test_spec();
3722 let (schema, _) =
3723 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, true, false)
3724 .unwrap();
3725
3726 assert!(schema.get("description").is_none());
3728
3729 assert_eq!(schema.get("type").unwrap(), "string");
3732 assert!(schema.get("example").is_none());
3733 assert_eq!(schema.get("examples").unwrap(), &json!(["available"]));
3734
3735 insta::assert_json_snapshot!("test_skip_parameter_descriptions", schema);
3736 }
3737
3738 #[test]
3739 fn test_keep_parameter_descriptions() {
3740 let param = Parameter {
3741 name: "status".to_string(),
3742 location: ParameterIn::Query,
3743 description: Some("Filter by status".to_string()),
3744 required: Some(false),
3745 deprecated: Some(false),
3746 allow_empty_value: Some(false),
3747 style: None,
3748 explode: None,
3749 allow_reserved: Some(false),
3750 schema: Some(ObjectOrReference::Object(ObjectSchema {
3751 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3752 enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3753 ..Default::default()
3754 })),
3755 example: Some(json!("available")),
3756 examples: Default::default(),
3757 content: None,
3758 extensions: Default::default(),
3759 };
3760
3761 let spec = create_test_spec();
3762 let (schema, _) = ToolGenerator::convert_parameter_schema(
3763 ¶m,
3764 ParameterIn::Query,
3765 &spec,
3766 false,
3767 false,
3768 )
3769 .unwrap();
3770
3771 assert!(schema.get("description").is_some());
3774 let description = schema.get("description").unwrap().as_str().unwrap();
3775 assert!(description.contains("Filter by status"));
3776 assert!(!description.contains("Example:"));
3777
3778 assert_eq!(schema.get("type").unwrap(), "string");
3780 assert!(schema.get("example").is_none());
3781 assert_eq!(schema.get("examples").unwrap(), &json!(["available"]));
3782
3783 insta::assert_json_snapshot!("test_keep_parameter_descriptions", schema);
3784 }
3785
3786 #[test]
3787 fn test_array_with_regular_items_schema() {
3788 let param = Parameter {
3790 name: "tags".to_string(),
3791 location: ParameterIn::Query,
3792 description: Some("List of tags".to_string()),
3793 required: Some(false),
3794 deprecated: Some(false),
3795 allow_empty_value: Some(false),
3796 style: None,
3797 explode: None,
3798 allow_reserved: Some(false),
3799 schema: Some(ObjectOrReference::Object(ObjectSchema {
3800 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3801 items: Some(Box::new(Schema::Object(Box::new(
3802 ObjectOrReference::Object(ObjectSchema {
3803 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3804 min_length: Some(1),
3805 max_length: Some(50),
3806 ..Default::default()
3807 }),
3808 )))),
3809 ..Default::default()
3810 })),
3811 example: None,
3812 examples: Default::default(),
3813 content: None,
3814 extensions: Default::default(),
3815 };
3816
3817 let spec = create_test_spec();
3818 let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3819 ¶m,
3820 ParameterIn::Query,
3821 &spec,
3822 false,
3823 false,
3824 )
3825 .unwrap();
3826
3827 insta::assert_json_snapshot!("test_array_with_regular_items_schema", result);
3829 }
3830
3831 #[test]
3832 fn test_request_body_object_schema() {
3833 let operation = Operation {
3835 operation_id: Some("createPet".to_string()),
3836 summary: Some("Create a new pet".to_string()),
3837 description: Some("Creates a new pet in the store".to_string()),
3838 tags: vec![],
3839 external_docs: None,
3840 parameters: vec![],
3841 request_body: Some(ObjectOrReference::Object(RequestBody {
3842 description: Some("Pet object that needs to be added to the store".to_string()),
3843 content: {
3844 let mut content = BTreeMap::new();
3845 content.insert(
3846 "application/json".to_string(),
3847 MediaType {
3848 extensions: Default::default(),
3849 schema: Some(ObjectOrReference::Object(ObjectSchema {
3850 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3851 ..Default::default()
3852 })),
3853 examples: None,
3854 encoding: Default::default(),
3855 },
3856 );
3857 content
3858 },
3859 required: Some(true),
3860 })),
3861 responses: Default::default(),
3862 callbacks: Default::default(),
3863 deprecated: Some(false),
3864 security: vec![],
3865 servers: vec![],
3866 extensions: Default::default(),
3867 };
3868
3869 let spec = create_test_spec();
3870 let metadata = ToolGenerator::generate_tool_metadata(
3871 &operation,
3872 "post".to_string(),
3873 "/pets".to_string(),
3874 &spec,
3875 false,
3876 false,
3877 false,
3878 )
3879 .unwrap();
3880
3881 let properties = metadata
3883 .parameters
3884 .get("properties")
3885 .unwrap()
3886 .as_object()
3887 .unwrap();
3888 assert!(properties.contains_key("request_body"));
3889
3890 let required = metadata
3892 .parameters
3893 .get("required")
3894 .unwrap()
3895 .as_array()
3896 .unwrap();
3897 assert!(required.contains(&json!("request_body")));
3898
3899 let request_body_schema = properties.get("request_body").unwrap();
3901 insta::assert_json_snapshot!("test_request_body_object_schema", request_body_schema);
3902
3903 validate_tool_against_mcp_schema(&metadata);
3905 }
3906
3907 #[test]
3908 fn test_request_body_array_schema() {
3909 let operation = Operation {
3911 operation_id: Some("createPets".to_string()),
3912 summary: Some("Create multiple pets".to_string()),
3913 description: None,
3914 tags: vec![],
3915 external_docs: None,
3916 parameters: vec![],
3917 request_body: Some(ObjectOrReference::Object(RequestBody {
3918 description: Some("Array of pet objects".to_string()),
3919 content: {
3920 let mut content = BTreeMap::new();
3921 content.insert(
3922 "application/json".to_string(),
3923 MediaType {
3924 extensions: Default::default(),
3925 schema: Some(ObjectOrReference::Object(ObjectSchema {
3926 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3927 items: Some(Box::new(Schema::Object(Box::new(
3928 ObjectOrReference::Object(ObjectSchema {
3929 schema_type: Some(SchemaTypeSet::Single(
3930 SchemaType::Object,
3931 )),
3932 ..Default::default()
3933 }),
3934 )))),
3935 ..Default::default()
3936 })),
3937 examples: None,
3938 encoding: Default::default(),
3939 },
3940 );
3941 content
3942 },
3943 required: Some(false),
3944 })),
3945 responses: Default::default(),
3946 callbacks: Default::default(),
3947 deprecated: Some(false),
3948 security: vec![],
3949 servers: vec![],
3950 extensions: Default::default(),
3951 };
3952
3953 let spec = create_test_spec();
3954 let metadata = ToolGenerator::generate_tool_metadata(
3955 &operation,
3956 "post".to_string(),
3957 "/pets/batch".to_string(),
3958 &spec,
3959 false,
3960 false,
3961 false,
3962 )
3963 .unwrap();
3964
3965 let properties = metadata
3967 .parameters
3968 .get("properties")
3969 .unwrap()
3970 .as_object()
3971 .unwrap();
3972 assert!(properties.contains_key("request_body"));
3973
3974 let required = metadata
3976 .parameters
3977 .get("required")
3978 .unwrap()
3979 .as_array()
3980 .unwrap();
3981 assert!(!required.contains(&json!("request_body")));
3982
3983 let request_body_schema = properties.get("request_body").unwrap();
3985 insta::assert_json_snapshot!("test_request_body_array_schema", request_body_schema);
3986
3987 validate_tool_against_mcp_schema(&metadata);
3989 }
3990
3991 #[test]
3992 fn test_request_body_string_schema() {
3993 let operation = Operation {
3995 operation_id: Some("updatePetName".to_string()),
3996 summary: Some("Update pet name".to_string()),
3997 description: None,
3998 tags: vec![],
3999 external_docs: None,
4000 parameters: vec![],
4001 request_body: Some(ObjectOrReference::Object(RequestBody {
4002 description: None,
4003 content: {
4004 let mut content = BTreeMap::new();
4005 content.insert(
4006 "text/plain".to_string(),
4007 MediaType {
4008 extensions: Default::default(),
4009 schema: Some(ObjectOrReference::Object(ObjectSchema {
4010 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4011 min_length: Some(1),
4012 max_length: Some(100),
4013 ..Default::default()
4014 })),
4015 examples: None,
4016 encoding: Default::default(),
4017 },
4018 );
4019 content
4020 },
4021 required: Some(true),
4022 })),
4023 responses: Default::default(),
4024 callbacks: Default::default(),
4025 deprecated: Some(false),
4026 security: vec![],
4027 servers: vec![],
4028 extensions: Default::default(),
4029 };
4030
4031 let spec = create_test_spec();
4032 let metadata = ToolGenerator::generate_tool_metadata(
4033 &operation,
4034 "put".to_string(),
4035 "/pets/{petId}/name".to_string(),
4036 &spec,
4037 false,
4038 false,
4039 false,
4040 )
4041 .unwrap();
4042
4043 let properties = metadata
4045 .parameters
4046 .get("properties")
4047 .unwrap()
4048 .as_object()
4049 .unwrap();
4050 let request_body_schema = properties.get("request_body").unwrap();
4051 insta::assert_json_snapshot!("test_request_body_string_schema", request_body_schema);
4052
4053 validate_tool_against_mcp_schema(&metadata);
4055 }
4056
4057 #[test]
4058 fn test_request_body_ref_schema() {
4059 let operation = Operation {
4061 operation_id: Some("updatePet".to_string()),
4062 summary: Some("Update existing pet".to_string()),
4063 description: None,
4064 tags: vec![],
4065 external_docs: None,
4066 parameters: vec![],
4067 request_body: Some(ObjectOrReference::Ref {
4068 ref_path: "#/components/requestBodies/PetBody".to_string(),
4069 summary: None,
4070 description: None,
4071 }),
4072 responses: Default::default(),
4073 callbacks: Default::default(),
4074 deprecated: Some(false),
4075 security: vec![],
4076 servers: vec![],
4077 extensions: Default::default(),
4078 };
4079
4080 let spec = create_test_spec();
4081 let metadata = ToolGenerator::generate_tool_metadata(
4082 &operation,
4083 "put".to_string(),
4084 "/pets/{petId}".to_string(),
4085 &spec,
4086 false,
4087 false,
4088 false,
4089 )
4090 .unwrap();
4091
4092 let properties = metadata
4094 .parameters
4095 .get("properties")
4096 .unwrap()
4097 .as_object()
4098 .unwrap();
4099 let request_body_schema = properties.get("request_body").unwrap();
4100 insta::assert_json_snapshot!("test_request_body_ref_schema", request_body_schema);
4101
4102 validate_tool_against_mcp_schema(&metadata);
4104 }
4105
4106 #[test]
4107 fn test_no_request_body_for_get() {
4108 let operation = Operation {
4110 operation_id: Some("listPets".to_string()),
4111 summary: Some("List all pets".to_string()),
4112 description: None,
4113 tags: vec![],
4114 external_docs: None,
4115 parameters: vec![],
4116 request_body: None,
4117 responses: Default::default(),
4118 callbacks: Default::default(),
4119 deprecated: Some(false),
4120 security: vec![],
4121 servers: vec![],
4122 extensions: Default::default(),
4123 };
4124
4125 let spec = create_test_spec();
4126 let metadata = ToolGenerator::generate_tool_metadata(
4127 &operation,
4128 "get".to_string(),
4129 "/pets".to_string(),
4130 &spec,
4131 false,
4132 false,
4133 false,
4134 )
4135 .unwrap();
4136
4137 let properties = metadata
4139 .parameters
4140 .get("properties")
4141 .unwrap()
4142 .as_object()
4143 .unwrap();
4144 assert!(!properties.contains_key("request_body"));
4145
4146 validate_tool_against_mcp_schema(&metadata);
4148 }
4149
4150 #[test]
4151 fn test_request_body_simple_object_with_properties() {
4152 let operation = Operation {
4154 operation_id: Some("updatePetStatus".to_string()),
4155 summary: Some("Update pet status".to_string()),
4156 description: None,
4157 tags: vec![],
4158 external_docs: None,
4159 parameters: vec![],
4160 request_body: Some(ObjectOrReference::Object(RequestBody {
4161 description: Some("Pet status update".to_string()),
4162 content: {
4163 let mut content = BTreeMap::new();
4164 content.insert(
4165 "application/json".to_string(),
4166 MediaType {
4167 extensions: Default::default(),
4168 schema: Some(ObjectOrReference::Object(ObjectSchema {
4169 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4170 properties: {
4171 let mut props = BTreeMap::new();
4172 props.insert(
4173 "status".to_string(),
4174 ObjectOrReference::Object(ObjectSchema {
4175 schema_type: Some(SchemaTypeSet::Single(
4176 SchemaType::String,
4177 )),
4178 ..Default::default()
4179 }),
4180 );
4181 props.insert(
4182 "reason".to_string(),
4183 ObjectOrReference::Object(ObjectSchema {
4184 schema_type: Some(SchemaTypeSet::Single(
4185 SchemaType::String,
4186 )),
4187 ..Default::default()
4188 }),
4189 );
4190 props
4191 },
4192 required: vec!["status".to_string()],
4193 ..Default::default()
4194 })),
4195 examples: None,
4196 encoding: Default::default(),
4197 },
4198 );
4199 content
4200 },
4201 required: Some(false),
4202 })),
4203 responses: Default::default(),
4204 callbacks: Default::default(),
4205 deprecated: Some(false),
4206 security: vec![],
4207 servers: vec![],
4208 extensions: Default::default(),
4209 };
4210
4211 let spec = create_test_spec();
4212 let metadata = ToolGenerator::generate_tool_metadata(
4213 &operation,
4214 "patch".to_string(),
4215 "/pets/{petId}/status".to_string(),
4216 &spec,
4217 false,
4218 false,
4219 false,
4220 )
4221 .unwrap();
4222
4223 let properties = metadata
4225 .parameters
4226 .get("properties")
4227 .unwrap()
4228 .as_object()
4229 .unwrap();
4230 let request_body_schema = properties.get("request_body").unwrap();
4231 insta::assert_json_snapshot!(
4232 "test_request_body_simple_object_with_properties",
4233 request_body_schema
4234 );
4235
4236 let required = metadata
4238 .parameters
4239 .get("required")
4240 .unwrap()
4241 .as_array()
4242 .unwrap();
4243 assert!(!required.contains(&json!("request_body")));
4244
4245 validate_tool_against_mcp_schema(&metadata);
4247 }
4248
4249 #[test]
4250 fn test_request_body_with_nested_properties() {
4251 let operation = Operation {
4253 operation_id: Some("createUser".to_string()),
4254 summary: Some("Create a new user".to_string()),
4255 description: None,
4256 tags: vec![],
4257 external_docs: None,
4258 parameters: vec![],
4259 request_body: Some(ObjectOrReference::Object(RequestBody {
4260 description: Some("User creation data".to_string()),
4261 content: {
4262 let mut content = BTreeMap::new();
4263 content.insert(
4264 "application/json".to_string(),
4265 MediaType {
4266 extensions: Default::default(),
4267 schema: Some(ObjectOrReference::Object(ObjectSchema {
4268 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4269 properties: {
4270 let mut props = BTreeMap::new();
4271 props.insert(
4272 "name".to_string(),
4273 ObjectOrReference::Object(ObjectSchema {
4274 schema_type: Some(SchemaTypeSet::Single(
4275 SchemaType::String,
4276 )),
4277 ..Default::default()
4278 }),
4279 );
4280 props.insert(
4281 "age".to_string(),
4282 ObjectOrReference::Object(ObjectSchema {
4283 schema_type: Some(SchemaTypeSet::Single(
4284 SchemaType::Integer,
4285 )),
4286 minimum: Some(serde_json::Number::from(0)),
4287 maximum: Some(serde_json::Number::from(150)),
4288 ..Default::default()
4289 }),
4290 );
4291 props
4292 },
4293 required: vec!["name".to_string()],
4294 ..Default::default()
4295 })),
4296 examples: None,
4297 encoding: Default::default(),
4298 },
4299 );
4300 content
4301 },
4302 required: Some(true),
4303 })),
4304 responses: Default::default(),
4305 callbacks: Default::default(),
4306 deprecated: Some(false),
4307 security: vec![],
4308 servers: vec![],
4309 extensions: Default::default(),
4310 };
4311
4312 let spec = create_test_spec();
4313 let metadata = ToolGenerator::generate_tool_metadata(
4314 &operation,
4315 "post".to_string(),
4316 "/users".to_string(),
4317 &spec,
4318 false,
4319 false,
4320 false,
4321 )
4322 .unwrap();
4323
4324 let properties = metadata
4326 .parameters
4327 .get("properties")
4328 .unwrap()
4329 .as_object()
4330 .unwrap();
4331 let request_body_schema = properties.get("request_body").unwrap();
4332 insta::assert_json_snapshot!(
4333 "test_request_body_with_nested_properties",
4334 request_body_schema
4335 );
4336
4337 validate_tool_against_mcp_schema(&metadata);
4339 }
4340
4341 #[test]
4342 fn test_operation_without_responses_has_no_output_schema() {
4343 let operation = Operation {
4344 operation_id: Some("testOperation".to_string()),
4345 summary: Some("Test operation".to_string()),
4346 description: None,
4347 tags: vec![],
4348 external_docs: None,
4349 parameters: vec![],
4350 request_body: None,
4351 responses: None,
4352 callbacks: Default::default(),
4353 deprecated: Some(false),
4354 security: vec![],
4355 servers: vec![],
4356 extensions: Default::default(),
4357 };
4358
4359 let spec = create_test_spec();
4360 let metadata = ToolGenerator::generate_tool_metadata(
4361 &operation,
4362 "get".to_string(),
4363 "/test".to_string(),
4364 &spec,
4365 false,
4366 false,
4367 false,
4368 )
4369 .unwrap();
4370
4371 assert!(metadata.output_schema.is_none());
4373
4374 validate_tool_against_mcp_schema(&metadata);
4376 }
4377
4378 #[test]
4379 fn test_extract_output_schema_with_200_response() {
4380 use oas3::spec::Response;
4381
4382 let mut responses = BTreeMap::new();
4384 let mut content = BTreeMap::new();
4385 content.insert(
4386 "application/json".to_string(),
4387 MediaType {
4388 extensions: Default::default(),
4389 schema: Some(ObjectOrReference::Object(ObjectSchema {
4390 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4391 properties: {
4392 let mut props = BTreeMap::new();
4393 props.insert(
4394 "id".to_string(),
4395 ObjectOrReference::Object(ObjectSchema {
4396 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4397 ..Default::default()
4398 }),
4399 );
4400 props.insert(
4401 "name".to_string(),
4402 ObjectOrReference::Object(ObjectSchema {
4403 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4404 ..Default::default()
4405 }),
4406 );
4407 props
4408 },
4409 required: vec!["id".to_string(), "name".to_string()],
4410 ..Default::default()
4411 })),
4412 examples: None,
4413 encoding: Default::default(),
4414 },
4415 );
4416
4417 responses.insert(
4418 "200".to_string(),
4419 ObjectOrReference::Object(Response {
4420 description: Some("Successful response".to_string()),
4421 headers: Default::default(),
4422 content,
4423 links: Default::default(),
4424 extensions: Default::default(),
4425 }),
4426 );
4427
4428 let spec = create_test_spec();
4429 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4430
4431 insta::assert_json_snapshot!(result);
4433 }
4434
4435 #[test]
4436 fn test_extract_output_schema_with_201_response() {
4437 use oas3::spec::Response;
4438
4439 let mut responses = BTreeMap::new();
4441 let mut content = BTreeMap::new();
4442 content.insert(
4443 "application/json".to_string(),
4444 MediaType {
4445 extensions: Default::default(),
4446 schema: Some(ObjectOrReference::Object(ObjectSchema {
4447 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4448 properties: {
4449 let mut props = BTreeMap::new();
4450 props.insert(
4451 "created".to_string(),
4452 ObjectOrReference::Object(ObjectSchema {
4453 schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
4454 ..Default::default()
4455 }),
4456 );
4457 props
4458 },
4459 ..Default::default()
4460 })),
4461 examples: None,
4462 encoding: Default::default(),
4463 },
4464 );
4465
4466 responses.insert(
4467 "201".to_string(),
4468 ObjectOrReference::Object(Response {
4469 description: Some("Created".to_string()),
4470 headers: Default::default(),
4471 content,
4472 links: Default::default(),
4473 extensions: Default::default(),
4474 }),
4475 );
4476
4477 let spec = create_test_spec();
4478 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4479
4480 insta::assert_json_snapshot!(result);
4482 }
4483
4484 #[test]
4485 fn test_extract_output_schema_with_2xx_response() {
4486 use oas3::spec::Response;
4487
4488 let mut responses = BTreeMap::new();
4490 let mut content = BTreeMap::new();
4491 content.insert(
4492 "application/json".to_string(),
4493 MediaType {
4494 extensions: Default::default(),
4495 schema: Some(ObjectOrReference::Object(ObjectSchema {
4496 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
4497 items: Some(Box::new(Schema::Object(Box::new(
4498 ObjectOrReference::Object(ObjectSchema {
4499 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4500 ..Default::default()
4501 }),
4502 )))),
4503 ..Default::default()
4504 })),
4505 examples: None,
4506 encoding: Default::default(),
4507 },
4508 );
4509
4510 responses.insert(
4511 "2XX".to_string(),
4512 ObjectOrReference::Object(Response {
4513 description: Some("Success".to_string()),
4514 headers: Default::default(),
4515 content,
4516 links: Default::default(),
4517 extensions: Default::default(),
4518 }),
4519 );
4520
4521 let spec = create_test_spec();
4522 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4523
4524 insta::assert_json_snapshot!(result);
4526 }
4527
4528 #[test]
4529 fn test_extract_output_schema_no_responses() {
4530 let spec = create_test_spec();
4531 let result = ToolGenerator::extract_output_schema(&None, &spec).unwrap();
4532
4533 insta::assert_json_snapshot!(result);
4535 }
4536
4537 #[test]
4538 fn test_extract_output_schema_only_error_responses() {
4539 use oas3::spec::Response;
4540
4541 let mut responses = BTreeMap::new();
4543 responses.insert(
4544 "404".to_string(),
4545 ObjectOrReference::Object(Response {
4546 description: Some("Not found".to_string()),
4547 headers: Default::default(),
4548 content: Default::default(),
4549 links: Default::default(),
4550 extensions: Default::default(),
4551 }),
4552 );
4553 responses.insert(
4554 "500".to_string(),
4555 ObjectOrReference::Object(Response {
4556 description: Some("Server error".to_string()),
4557 headers: Default::default(),
4558 content: Default::default(),
4559 links: Default::default(),
4560 extensions: Default::default(),
4561 }),
4562 );
4563
4564 let spec = create_test_spec();
4565 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4566
4567 insta::assert_json_snapshot!(result);
4569 }
4570
4571 #[test]
4572 fn test_extract_output_schema_with_ref() {
4573 use oas3::spec::Response;
4574
4575 let mut spec = create_test_spec();
4577 let mut schemas = BTreeMap::new();
4578 schemas.insert(
4579 "Pet".to_string(),
4580 ObjectOrReference::Object(ObjectSchema {
4581 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4582 properties: {
4583 let mut props = BTreeMap::new();
4584 props.insert(
4585 "name".to_string(),
4586 ObjectOrReference::Object(ObjectSchema {
4587 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4588 ..Default::default()
4589 }),
4590 );
4591 props
4592 },
4593 ..Default::default()
4594 }),
4595 );
4596 spec.components.as_mut().unwrap().schemas = schemas;
4597
4598 let mut responses = BTreeMap::new();
4600 let mut content = BTreeMap::new();
4601 content.insert(
4602 "application/json".to_string(),
4603 MediaType {
4604 extensions: Default::default(),
4605 schema: Some(ObjectOrReference::Ref {
4606 ref_path: "#/components/schemas/Pet".to_string(),
4607 summary: None,
4608 description: None,
4609 }),
4610 examples: None,
4611 encoding: Default::default(),
4612 },
4613 );
4614
4615 responses.insert(
4616 "200".to_string(),
4617 ObjectOrReference::Object(Response {
4618 description: Some("Success".to_string()),
4619 headers: Default::default(),
4620 content,
4621 links: Default::default(),
4622 extensions: Default::default(),
4623 }),
4624 );
4625
4626 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4627
4628 insta::assert_json_snapshot!(result);
4630 }
4631
4632 #[test]
4633 fn test_generate_tool_metadata_includes_output_schema() {
4634 use oas3::spec::Response;
4635
4636 let mut operation = Operation {
4637 operation_id: Some("getPet".to_string()),
4638 summary: Some("Get a pet".to_string()),
4639 description: None,
4640 tags: vec![],
4641 external_docs: None,
4642 parameters: vec![],
4643 request_body: None,
4644 responses: Default::default(),
4645 callbacks: Default::default(),
4646 deprecated: Some(false),
4647 security: vec![],
4648 servers: vec![],
4649 extensions: Default::default(),
4650 };
4651
4652 let mut responses = BTreeMap::new();
4654 let mut content = BTreeMap::new();
4655 content.insert(
4656 "application/json".to_string(),
4657 MediaType {
4658 extensions: Default::default(),
4659 schema: Some(ObjectOrReference::Object(ObjectSchema {
4660 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4661 properties: {
4662 let mut props = BTreeMap::new();
4663 props.insert(
4664 "id".to_string(),
4665 ObjectOrReference::Object(ObjectSchema {
4666 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4667 ..Default::default()
4668 }),
4669 );
4670 props
4671 },
4672 ..Default::default()
4673 })),
4674 examples: None,
4675 encoding: Default::default(),
4676 },
4677 );
4678
4679 responses.insert(
4680 "200".to_string(),
4681 ObjectOrReference::Object(Response {
4682 description: Some("Success".to_string()),
4683 headers: Default::default(),
4684 content,
4685 links: Default::default(),
4686 extensions: Default::default(),
4687 }),
4688 );
4689 operation.responses = Some(responses);
4690
4691 let spec = create_test_spec();
4692 let metadata = ToolGenerator::generate_tool_metadata(
4693 &operation,
4694 "get".to_string(),
4695 "/pets/{id}".to_string(),
4696 &spec,
4697 false,
4698 false,
4699 false,
4700 )
4701 .unwrap();
4702
4703 assert!(metadata.output_schema.is_some());
4705 let output_schema = metadata.output_schema.as_ref().unwrap();
4706
4707 insta::assert_json_snapshot!(
4709 "test_generate_tool_metadata_includes_output_schema",
4710 output_schema
4711 );
4712
4713 validate_tool_against_mcp_schema(&metadata);
4715 }
4716
4717 #[test]
4718 fn test_sanitize_property_name() {
4719 assert_eq!(sanitize_property_name("user name"), "user_name");
4721 assert_eq!(
4722 sanitize_property_name("first name last name"),
4723 "first_name_last_name"
4724 );
4725
4726 assert_eq!(sanitize_property_name("user(admin)"), "user_admin");
4728 assert_eq!(sanitize_property_name("user[admin]"), "user_admin");
4729 assert_eq!(sanitize_property_name("price($)"), "price");
4730 assert_eq!(sanitize_property_name("email@address"), "email_address");
4731 assert_eq!(sanitize_property_name("item#1"), "item_1");
4732 assert_eq!(sanitize_property_name("a/b/c"), "a_b_c");
4733
4734 assert_eq!(sanitize_property_name("user_name"), "user_name");
4736 assert_eq!(sanitize_property_name("userName123"), "userName123");
4737 assert_eq!(sanitize_property_name("user.name"), "user.name");
4738 assert_eq!(sanitize_property_name("user-name"), "user-name");
4739
4740 assert_eq!(sanitize_property_name("123name"), "param_123name");
4742 assert_eq!(sanitize_property_name("1st_place"), "param_1st_place");
4743
4744 assert_eq!(sanitize_property_name(""), "param_");
4746
4747 let long_name = "a".repeat(100);
4749 assert_eq!(sanitize_property_name(&long_name).len(), 64);
4750
4751 assert_eq!(sanitize_property_name("!@#$%^&*()"), "param_");
4754 }
4755
4756 #[test]
4757 fn test_sanitize_property_name_trailing_underscores() {
4758 assert_eq!(sanitize_property_name("page[size]"), "page_size");
4760 assert_eq!(sanitize_property_name("user[id]"), "user_id");
4761 assert_eq!(sanitize_property_name("field[]"), "field");
4762
4763 assert_eq!(sanitize_property_name("field___"), "field");
4765 assert_eq!(sanitize_property_name("test[[["), "test");
4766 }
4767
4768 #[test]
4769 fn test_sanitize_property_name_consecutive_underscores() {
4770 assert_eq!(sanitize_property_name("user__name"), "user_name");
4772 assert_eq!(sanitize_property_name("first___last"), "first_last");
4773 assert_eq!(sanitize_property_name("a____b____c"), "a_b_c");
4774
4775 assert_eq!(sanitize_property_name("user[[name]]"), "user_name");
4777 assert_eq!(sanitize_property_name("field@#$value"), "field_value");
4778 }
4779
4780 #[test]
4781 fn test_sanitize_property_name_edge_cases() {
4782 assert_eq!(sanitize_property_name("_private"), "_private");
4784 assert_eq!(sanitize_property_name("__dunder"), "_dunder");
4785
4786 assert_eq!(sanitize_property_name("[[["), "param_");
4788 assert_eq!(sanitize_property_name("@@@"), "param_");
4789
4790 assert_eq!(sanitize_property_name(""), "param_");
4792
4793 assert_eq!(sanitize_property_name("_field[size]"), "_field_size");
4795 assert_eq!(sanitize_property_name("__test__"), "_test");
4796 }
4797
4798 #[test]
4799 fn test_sanitize_property_name_complex_cases() {
4800 assert_eq!(sanitize_property_name("page[size]"), "page_size");
4802 assert_eq!(sanitize_property_name("filter[status]"), "filter_status");
4803 assert_eq!(
4804 sanitize_property_name("sort[-created_at]"),
4805 "sort_-created_at"
4806 );
4807 assert_eq!(
4808 sanitize_property_name("include[author.posts]"),
4809 "include_author.posts"
4810 );
4811
4812 let long_name = "very_long_field_name_with_special[characters]_that_needs_truncation_____";
4814 let expected = "very_long_field_name_with_special_characters_that_needs_truncat";
4815 assert_eq!(sanitize_property_name(long_name), expected);
4816 }
4817
4818 #[test]
4819 fn test_property_sanitization_with_annotations() {
4820 let spec = create_test_spec();
4821 let mut visited = HashSet::new();
4822
4823 let obj_schema = ObjectSchema {
4825 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4826 properties: {
4827 let mut props = BTreeMap::new();
4828 props.insert(
4830 "user name".to_string(),
4831 ObjectOrReference::Object(ObjectSchema {
4832 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4833 ..Default::default()
4834 }),
4835 );
4836 props.insert(
4838 "price($)".to_string(),
4839 ObjectOrReference::Object(ObjectSchema {
4840 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
4841 ..Default::default()
4842 }),
4843 );
4844 props.insert(
4846 "validName".to_string(),
4847 ObjectOrReference::Object(ObjectSchema {
4848 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4849 ..Default::default()
4850 }),
4851 );
4852 props
4853 },
4854 ..Default::default()
4855 };
4856
4857 let result =
4858 ToolGenerator::convert_object_schema_to_json_schema(&obj_schema, &spec, &mut visited)
4859 .unwrap();
4860
4861 insta::assert_json_snapshot!("test_property_sanitization_with_annotations", result);
4863 }
4864
4865 #[test]
4866 fn test_parameter_sanitization_and_extraction() {
4867 let spec = create_test_spec();
4868
4869 let operation = Operation {
4871 operation_id: Some("testOp".to_string()),
4872 parameters: vec![
4873 ObjectOrReference::Object(Parameter {
4875 name: "user(id)".to_string(),
4876 location: ParameterIn::Path,
4877 description: Some("User ID".to_string()),
4878 required: Some(true),
4879 deprecated: Some(false),
4880 allow_empty_value: Some(false),
4881 style: None,
4882 explode: None,
4883 allow_reserved: Some(false),
4884 schema: Some(ObjectOrReference::Object(ObjectSchema {
4885 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4886 ..Default::default()
4887 })),
4888 example: None,
4889 examples: Default::default(),
4890 content: None,
4891 extensions: Default::default(),
4892 }),
4893 ObjectOrReference::Object(Parameter {
4895 name: "page size".to_string(),
4896 location: ParameterIn::Query,
4897 description: Some("Page size".to_string()),
4898 required: Some(false),
4899 deprecated: Some(false),
4900 allow_empty_value: Some(false),
4901 style: None,
4902 explode: None,
4903 allow_reserved: Some(false),
4904 schema: Some(ObjectOrReference::Object(ObjectSchema {
4905 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4906 ..Default::default()
4907 })),
4908 example: None,
4909 examples: Default::default(),
4910 content: None,
4911 extensions: Default::default(),
4912 }),
4913 ObjectOrReference::Object(Parameter {
4915 name: "auth-token!".to_string(),
4916 location: ParameterIn::Header,
4917 description: Some("Auth token".to_string()),
4918 required: Some(false),
4919 deprecated: Some(false),
4920 allow_empty_value: Some(false),
4921 style: None,
4922 explode: None,
4923 allow_reserved: Some(false),
4924 schema: Some(ObjectOrReference::Object(ObjectSchema {
4925 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4926 ..Default::default()
4927 })),
4928 example: None,
4929 examples: Default::default(),
4930 content: None,
4931 extensions: Default::default(),
4932 }),
4933 ],
4934 ..Default::default()
4935 };
4936
4937 let tool_metadata = ToolGenerator::generate_tool_metadata(
4938 &operation,
4939 "get".to_string(),
4940 "/users/{user(id)}".to_string(),
4941 &spec,
4942 false,
4943 false,
4944 false,
4945 )
4946 .unwrap();
4947
4948 let properties = tool_metadata
4950 .parameters
4951 .get("properties")
4952 .unwrap()
4953 .as_object()
4954 .unwrap();
4955
4956 assert!(properties.contains_key("user_id"));
4957 assert!(properties.contains_key("page_size"));
4958 assert!(properties.contains_key("header_auth-token"));
4959
4960 let required = tool_metadata
4962 .parameters
4963 .get("required")
4964 .unwrap()
4965 .as_array()
4966 .unwrap();
4967 assert!(required.contains(&json!("user_id")));
4968
4969 let arguments = json!({
4971 "user_id": "123",
4972 "page_size": 10,
4973 "header_auth-token": "secret"
4974 });
4975
4976 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4977
4978 assert_eq!(extracted.path.get("user(id)"), Some(&json!("123")));
4980
4981 assert_eq!(
4983 extracted.query.get("page size").map(|q| &q.value),
4984 Some(&json!(10))
4985 );
4986
4987 assert_eq!(extracted.headers.get("auth-token!"), Some(&json!("secret")));
4989 }
4990
4991 #[test]
4992 fn test_check_unknown_parameters() {
4993 let mut properties = serde_json::Map::new();
4995 properties.insert("page_size".to_string(), json!({"type": "integer"}));
4996 properties.insert("user_id".to_string(), json!({"type": "string"}));
4997
4998 let mut args = serde_json::Map::new();
4999 args.insert("page_sixe".to_string(), json!(10)); let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5002 assert!(!result.is_empty());
5003 assert_eq!(result.len(), 1);
5004
5005 match &result[0] {
5006 ValidationError::InvalidParameter {
5007 parameter,
5008 suggestions,
5009 valid_parameters,
5010 } => {
5011 assert_eq!(parameter, "page_sixe");
5012 assert_eq!(suggestions, &vec!["page_size".to_string()]);
5013 assert_eq!(
5014 valid_parameters,
5015 &vec!["page_size".to_string(), "user_id".to_string()]
5016 );
5017 }
5018 _ => panic!("Expected InvalidParameter variant"),
5019 }
5020 }
5021
5022 #[test]
5023 fn test_check_unknown_parameters_no_suggestions() {
5024 let mut properties = serde_json::Map::new();
5026 properties.insert("limit".to_string(), json!({"type": "integer"}));
5027 properties.insert("offset".to_string(), json!({"type": "integer"}));
5028
5029 let mut args = serde_json::Map::new();
5030 args.insert("xyz123".to_string(), json!("value"));
5031
5032 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5033 assert!(!result.is_empty());
5034 assert_eq!(result.len(), 1);
5035
5036 match &result[0] {
5037 ValidationError::InvalidParameter {
5038 parameter,
5039 suggestions,
5040 valid_parameters,
5041 } => {
5042 assert_eq!(parameter, "xyz123");
5043 assert!(suggestions.is_empty());
5044 assert!(valid_parameters.contains(&"limit".to_string()));
5045 assert!(valid_parameters.contains(&"offset".to_string()));
5046 }
5047 _ => panic!("Expected InvalidParameter variant"),
5048 }
5049 }
5050
5051 #[test]
5052 fn test_check_unknown_parameters_multiple_suggestions() {
5053 let mut properties = serde_json::Map::new();
5055 properties.insert("user_id".to_string(), json!({"type": "string"}));
5056 properties.insert("user_iid".to_string(), json!({"type": "string"}));
5057 properties.insert("user_name".to_string(), json!({"type": "string"}));
5058
5059 let mut args = serde_json::Map::new();
5060 args.insert("usr_id".to_string(), json!("123"));
5061
5062 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5063 assert!(!result.is_empty());
5064 assert_eq!(result.len(), 1);
5065
5066 match &result[0] {
5067 ValidationError::InvalidParameter {
5068 parameter,
5069 suggestions,
5070 valid_parameters,
5071 } => {
5072 assert_eq!(parameter, "usr_id");
5073 assert!(!suggestions.is_empty());
5074 assert!(suggestions.contains(&"user_id".to_string()));
5075 assert_eq!(valid_parameters.len(), 3);
5076 }
5077 _ => panic!("Expected InvalidParameter variant"),
5078 }
5079 }
5080
5081 #[test]
5082 fn test_check_unknown_parameters_valid() {
5083 let mut properties = serde_json::Map::new();
5085 properties.insert("name".to_string(), json!({"type": "string"}));
5086 properties.insert("email".to_string(), json!({"type": "string"}));
5087
5088 let mut args = serde_json::Map::new();
5089 args.insert("name".to_string(), json!("John"));
5090 args.insert("email".to_string(), json!("john@example.com"));
5091
5092 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5093 assert!(result.is_empty());
5094 }
5095
5096 #[test]
5097 fn test_check_unknown_parameters_empty() {
5098 let properties = serde_json::Map::new();
5100
5101 let mut args = serde_json::Map::new();
5102 args.insert("any_param".to_string(), json!("value"));
5103
5104 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5105 assert!(!result.is_empty());
5106 assert_eq!(result.len(), 1);
5107
5108 match &result[0] {
5109 ValidationError::InvalidParameter {
5110 parameter,
5111 suggestions,
5112 valid_parameters,
5113 } => {
5114 assert_eq!(parameter, "any_param");
5115 assert!(suggestions.is_empty());
5116 assert!(valid_parameters.is_empty());
5117 }
5118 _ => panic!("Expected InvalidParameter variant"),
5119 }
5120 }
5121
5122 #[test]
5123 fn test_check_unknown_parameters_gltf_pagination() {
5124 let mut properties = serde_json::Map::new();
5126 properties.insert(
5127 "page_number".to_string(),
5128 json!({
5129 "type": "integer",
5130 "x-original-name": "page[number]"
5131 }),
5132 );
5133 properties.insert(
5134 "page_size".to_string(),
5135 json!({
5136 "type": "integer",
5137 "x-original-name": "page[size]"
5138 }),
5139 );
5140
5141 let mut args = serde_json::Map::new();
5143 args.insert("page".to_string(), json!(1));
5144 args.insert("per_page".to_string(), json!(10));
5145
5146 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5147 assert_eq!(result.len(), 2, "Should have 2 unknown parameters");
5148
5149 let page_error = result
5151 .iter()
5152 .find(|e| {
5153 if let ValidationError::InvalidParameter { parameter, .. } = e {
5154 parameter == "page"
5155 } else {
5156 false
5157 }
5158 })
5159 .expect("Should have error for 'page'");
5160
5161 let per_page_error = result
5162 .iter()
5163 .find(|e| {
5164 if let ValidationError::InvalidParameter { parameter, .. } = e {
5165 parameter == "per_page"
5166 } else {
5167 false
5168 }
5169 })
5170 .expect("Should have error for 'per_page'");
5171
5172 match page_error {
5174 ValidationError::InvalidParameter {
5175 suggestions,
5176 valid_parameters,
5177 ..
5178 } => {
5179 assert!(
5180 suggestions.contains(&"page_number".to_string()),
5181 "Should suggest 'page_number' for 'page'"
5182 );
5183 assert_eq!(valid_parameters.len(), 2);
5184 assert!(valid_parameters.contains(&"page_number".to_string()));
5185 assert!(valid_parameters.contains(&"page_size".to_string()));
5186 }
5187 _ => panic!("Expected InvalidParameter"),
5188 }
5189
5190 match per_page_error {
5192 ValidationError::InvalidParameter {
5193 parameter,
5194 suggestions,
5195 valid_parameters,
5196 ..
5197 } => {
5198 assert_eq!(parameter, "per_page");
5199 assert_eq!(valid_parameters.len(), 2);
5200 if !suggestions.is_empty() {
5203 assert!(suggestions.contains(&"page_size".to_string()));
5204 }
5205 }
5206 _ => panic!("Expected InvalidParameter"),
5207 }
5208 }
5209
5210 #[test]
5211 fn test_validate_parameters_with_invalid_params() {
5212 let tool_metadata = ToolMetadata {
5214 name: "listItems".to_string(),
5215 title: None,
5216 description: Some("List items".to_string()),
5217 parameters: json!({
5218 "type": "object",
5219 "properties": {
5220 "page_number": {
5221 "type": "integer",
5222 "x-original-name": "page[number]"
5223 },
5224 "page_size": {
5225 "type": "integer",
5226 "x-original-name": "page[size]"
5227 }
5228 },
5229 "required": []
5230 }),
5231 output_schema: None,
5232 method: "GET".to_string(),
5233 path: "/items".to_string(),
5234 security: None,
5235 parameter_mappings: std::collections::HashMap::new(),
5236 };
5237
5238 let arguments = json!({
5240 "page": 1,
5241 "per_page": 10
5242 });
5243
5244 let result = ToolGenerator::validate_parameters(&tool_metadata, &arguments);
5245 assert!(
5246 result.is_err(),
5247 "Should fail validation with unknown parameters"
5248 );
5249
5250 let error = result.unwrap_err();
5251 match error {
5252 ToolCallValidationError::InvalidParameters { violations } => {
5253 assert_eq!(violations.len(), 2, "Should have 2 validation errors");
5254
5255 let has_page_error = violations.iter().any(|v| {
5257 if let ValidationError::InvalidParameter { parameter, .. } = v {
5258 parameter == "page"
5259 } else {
5260 false
5261 }
5262 });
5263
5264 let has_per_page_error = violations.iter().any(|v| {
5265 if let ValidationError::InvalidParameter { parameter, .. } = v {
5266 parameter == "per_page"
5267 } else {
5268 false
5269 }
5270 });
5271
5272 assert!(has_page_error, "Should have error for 'page' parameter");
5273 assert!(
5274 has_per_page_error,
5275 "Should have error for 'per_page' parameter"
5276 );
5277 }
5278 _ => panic!("Expected InvalidParameters"),
5279 }
5280 }
5281
5282 #[test]
5283 fn test_cookie_parameter_sanitization() {
5284 let spec = create_test_spec();
5285
5286 let operation = Operation {
5287 operation_id: Some("testCookie".to_string()),
5288 parameters: vec![ObjectOrReference::Object(Parameter {
5289 name: "session[id]".to_string(),
5290 location: ParameterIn::Cookie,
5291 description: Some("Session ID".to_string()),
5292 required: Some(false),
5293 deprecated: Some(false),
5294 allow_empty_value: Some(false),
5295 style: None,
5296 explode: None,
5297 allow_reserved: Some(false),
5298 schema: Some(ObjectOrReference::Object(ObjectSchema {
5299 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5300 ..Default::default()
5301 })),
5302 example: None,
5303 examples: Default::default(),
5304 content: None,
5305 extensions: Default::default(),
5306 })],
5307 ..Default::default()
5308 };
5309
5310 let tool_metadata = ToolGenerator::generate_tool_metadata(
5311 &operation,
5312 "get".to_string(),
5313 "/data".to_string(),
5314 &spec,
5315 false,
5316 false,
5317 false,
5318 )
5319 .unwrap();
5320
5321 let properties = tool_metadata
5322 .parameters
5323 .get("properties")
5324 .unwrap()
5325 .as_object()
5326 .unwrap();
5327
5328 assert!(properties.contains_key("cookie_session_id"));
5330
5331 let arguments = json!({
5333 "cookie_session_id": "abc123"
5334 });
5335
5336 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
5337
5338 assert_eq!(extracted.cookies.get("session[id]"), Some(&json!("abc123")));
5340 }
5341
5342 #[test]
5343 fn test_parameter_description_with_examples() {
5344 let spec = create_test_spec();
5345
5346 let param_with_example = Parameter {
5348 name: "status".to_string(),
5349 location: ParameterIn::Query,
5350 description: Some("Filter by status".to_string()),
5351 required: Some(false),
5352 deprecated: Some(false),
5353 allow_empty_value: Some(false),
5354 style: None,
5355 explode: None,
5356 allow_reserved: Some(false),
5357 schema: Some(ObjectOrReference::Object(ObjectSchema {
5358 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5359 ..Default::default()
5360 })),
5361 example: Some(json!("active")),
5362 examples: Default::default(),
5363 content: None,
5364 extensions: Default::default(),
5365 };
5366
5367 let (schema, _) = ToolGenerator::convert_parameter_schema(
5368 ¶m_with_example,
5369 ParameterIn::Query,
5370 &spec,
5371 false,
5372 true,
5373 )
5374 .unwrap();
5375 let description = schema.get("description").unwrap().as_str().unwrap();
5376 assert_eq!(description, "Filter by status. Example: `\"active\"`");
5377
5378 let mut examples_map = std::collections::BTreeMap::new();
5380 examples_map.insert(
5381 "example1".to_string(),
5382 ObjectOrReference::Object(oas3::spec::Example {
5383 value: Some(json!("pending")),
5384 ..Default::default()
5385 }),
5386 );
5387 examples_map.insert(
5388 "example2".to_string(),
5389 ObjectOrReference::Object(oas3::spec::Example {
5390 value: Some(json!("completed")),
5391 ..Default::default()
5392 }),
5393 );
5394
5395 let param_with_examples = Parameter {
5396 name: "status".to_string(),
5397 location: ParameterIn::Query,
5398 description: Some("Filter by status".to_string()),
5399 required: Some(false),
5400 deprecated: Some(false),
5401 allow_empty_value: Some(false),
5402 style: None,
5403 explode: None,
5404 allow_reserved: Some(false),
5405 schema: Some(ObjectOrReference::Object(ObjectSchema {
5406 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5407 ..Default::default()
5408 })),
5409 example: None,
5410 examples: examples_map,
5411 content: None,
5412 extensions: Default::default(),
5413 };
5414
5415 let (schema, _) = ToolGenerator::convert_parameter_schema(
5416 ¶m_with_examples,
5417 ParameterIn::Query,
5418 &spec,
5419 false,
5420 true,
5421 )
5422 .unwrap();
5423 let description = schema.get("description").unwrap().as_str().unwrap();
5424 assert!(description.starts_with("Filter by status. Examples:\n"));
5425 assert!(description.contains("`\"pending\"`"));
5426 assert!(description.contains("`\"completed\"`"));
5427
5428 let param_no_desc = Parameter {
5430 name: "limit".to_string(),
5431 location: ParameterIn::Query,
5432 description: None,
5433 required: Some(false),
5434 deprecated: Some(false),
5435 allow_empty_value: Some(false),
5436 style: None,
5437 explode: None,
5438 allow_reserved: Some(false),
5439 schema: Some(ObjectOrReference::Object(ObjectSchema {
5440 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
5441 ..Default::default()
5442 })),
5443 example: Some(json!(100)),
5444 examples: Default::default(),
5445 content: None,
5446 extensions: Default::default(),
5447 };
5448
5449 let (schema, _) = ToolGenerator::convert_parameter_schema(
5450 ¶m_no_desc,
5451 ParameterIn::Query,
5452 &spec,
5453 false,
5454 true,
5455 )
5456 .unwrap();
5457 let description = schema.get("description").unwrap().as_str().unwrap();
5458 assert_eq!(description, "limit parameter. Example: `100`");
5459 }
5460
5461 #[test]
5462 fn test_format_examples_for_description() {
5463 let examples = vec![json!("active")];
5465 let result = ToolGenerator::format_examples_for_description(&examples);
5466 assert_eq!(result, Some("Example: `\"active\"`".to_string()));
5467
5468 let examples = vec![json!(42)];
5470 let result = ToolGenerator::format_examples_for_description(&examples);
5471 assert_eq!(result, Some("Example: `42`".to_string()));
5472
5473 let examples = vec![json!(true)];
5475 let result = ToolGenerator::format_examples_for_description(&examples);
5476 assert_eq!(result, Some("Example: `true`".to_string()));
5477
5478 let examples = vec![json!("active"), json!("pending"), json!("completed")];
5480 let result = ToolGenerator::format_examples_for_description(&examples);
5481 assert_eq!(
5482 result,
5483 Some("Examples:\n- `\"active\"`\n- `\"pending\"`\n- `\"completed\"`".to_string())
5484 );
5485
5486 let examples = vec![json!(["a", "b", "c"])];
5488 let result = ToolGenerator::format_examples_for_description(&examples);
5489 assert_eq!(result, Some("Example: `[\"a\",\"b\",\"c\"]`".to_string()));
5490
5491 let examples = vec![json!({"key": "value"})];
5493 let result = ToolGenerator::format_examples_for_description(&examples);
5494 assert_eq!(result, Some("Example: `{\"key\":\"value\"}`".to_string()));
5495
5496 let examples = vec![];
5498 let result = ToolGenerator::format_examples_for_description(&examples);
5499 assert_eq!(result, None);
5500
5501 let examples = vec![json!(null)];
5503 let result = ToolGenerator::format_examples_for_description(&examples);
5504 assert_eq!(result, Some("Example: `null`".to_string()));
5505
5506 let examples = vec![json!("text"), json!(123), json!(true)];
5508 let result = ToolGenerator::format_examples_for_description(&examples);
5509 assert_eq!(
5510 result,
5511 Some("Examples:\n- `\"text\"`\n- `123`\n- `true`".to_string())
5512 );
5513
5514 let examples = vec![json!(["a", "b", "c", "d", "e", "f"])];
5516 let result = ToolGenerator::format_examples_for_description(&examples);
5517 assert_eq!(
5518 result,
5519 Some("Example: `[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"]`".to_string())
5520 );
5521
5522 let examples = vec![json!([1, 2])];
5524 let result = ToolGenerator::format_examples_for_description(&examples);
5525 assert_eq!(result, Some("Example: `[1,2]`".to_string()));
5526
5527 let examples = vec![json!({"user": {"name": "John", "age": 30}})];
5529 let result = ToolGenerator::format_examples_for_description(&examples);
5530 assert_eq!(
5531 result,
5532 Some("Example: `{\"user\":{\"name\":\"John\",\"age\":30}}`".to_string())
5533 );
5534
5535 let examples = vec![json!("a"), json!("b"), json!("c"), json!("d"), json!("e")];
5537 let result = ToolGenerator::format_examples_for_description(&examples);
5538 assert_eq!(
5539 result,
5540 Some("Examples:\n- `\"a\"`\n- `\"b\"`\n- `\"c\"`\n- `\"d\"`\n- `\"e\"`".to_string())
5541 );
5542
5543 let examples = vec![json!(3.5)];
5545 let result = ToolGenerator::format_examples_for_description(&examples);
5546 assert_eq!(result, Some("Example: `3.5`".to_string()));
5547
5548 let examples = vec![json!(-42)];
5550 let result = ToolGenerator::format_examples_for_description(&examples);
5551 assert_eq!(result, Some("Example: `-42`".to_string()));
5552
5553 let examples = vec![json!(false)];
5555 let result = ToolGenerator::format_examples_for_description(&examples);
5556 assert_eq!(result, Some("Example: `false`".to_string()));
5557
5558 let examples = vec![json!("hello \"world\"")];
5560 let result = ToolGenerator::format_examples_for_description(&examples);
5561 assert_eq!(result, Some(r#"Example: `"hello \"world\""`"#.to_string()));
5563
5564 let examples = vec![json!("")];
5566 let result = ToolGenerator::format_examples_for_description(&examples);
5567 assert_eq!(result, Some("Example: `\"\"`".to_string()));
5568
5569 let examples = vec![json!([])];
5571 let result = ToolGenerator::format_examples_for_description(&examples);
5572 assert_eq!(result, Some("Example: `[]`".to_string()));
5573
5574 let examples = vec![json!({})];
5576 let result = ToolGenerator::format_examples_for_description(&examples);
5577 assert_eq!(result, Some("Example: `{}`".to_string()));
5578 }
5579
5580 #[test]
5581 fn test_reference_metadata_functionality() {
5582 let metadata = ReferenceMetadata::new(
5584 Some("User Reference".to_string()),
5585 Some("A reference to user data with additional context".to_string()),
5586 );
5587
5588 assert!(!metadata.is_empty());
5589 assert_eq!(metadata.summary(), Some("User Reference"));
5590 assert_eq!(
5591 metadata.best_description(),
5592 Some("A reference to user data with additional context")
5593 );
5594
5595 let summary_only = ReferenceMetadata::new(Some("Pet Summary".to_string()), None);
5597 assert_eq!(summary_only.best_description(), Some("Pet Summary"));
5598
5599 let empty_metadata = ReferenceMetadata::new(None, None);
5601 assert!(empty_metadata.is_empty());
5602 assert_eq!(empty_metadata.best_description(), None);
5603
5604 let metadata = ReferenceMetadata::new(
5606 Some("Reference Summary".to_string()),
5607 Some("Reference Description".to_string()),
5608 );
5609
5610 let result = metadata.merge_with_description(None, false);
5612 assert_eq!(result, Some("Reference Description".to_string()));
5613
5614 let result = metadata.merge_with_description(Some("Existing desc"), false);
5616 assert_eq!(result, Some("Reference Description".to_string()));
5617
5618 let result = metadata.merge_with_description(Some("Existing desc"), true);
5620 assert_eq!(result, Some("Reference Description".to_string()));
5621
5622 let result = metadata.enhance_parameter_description("userId", Some("User ID parameter"));
5624 assert_eq!(result, Some("userId: Reference Description".to_string()));
5625
5626 let result = metadata.enhance_parameter_description("userId", None);
5627 assert_eq!(result, Some("userId: Reference Description".to_string()));
5628
5629 let summary_only = ReferenceMetadata::new(Some("API Token".to_string()), None);
5631
5632 let result = summary_only.merge_with_description(Some("Generic token"), false);
5633 assert_eq!(result, Some("API Token".to_string()));
5634
5635 let result = summary_only.merge_with_description(Some("Different desc"), true);
5636 assert_eq!(result, Some("API Token".to_string())); let result = summary_only.enhance_parameter_description("token", Some("Token field"));
5639 assert_eq!(result, Some("token: API Token".to_string()));
5640
5641 let empty_meta = ReferenceMetadata::new(None, None);
5643
5644 let result = empty_meta.merge_with_description(Some("Schema description"), false);
5645 assert_eq!(result, Some("Schema description".to_string()));
5646
5647 let result = empty_meta.enhance_parameter_description("param", Some("Schema param"));
5648 assert_eq!(result, Some("Schema param".to_string()));
5649
5650 let result = empty_meta.enhance_parameter_description("param", None);
5651 assert_eq!(result, Some("param parameter".to_string()));
5652 }
5653
5654 #[test]
5655 fn test_parameter_schema_with_reference_metadata() {
5656 let mut spec = create_test_spec();
5657
5658 spec.components.as_mut().unwrap().schemas.insert(
5660 "Pet".to_string(),
5661 ObjectOrReference::Object(ObjectSchema {
5662 description: None, schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5664 ..Default::default()
5665 }),
5666 );
5667
5668 let param_with_ref = Parameter {
5670 name: "user".to_string(),
5671 location: ParameterIn::Query,
5672 description: None,
5673 required: Some(true),
5674 deprecated: Some(false),
5675 allow_empty_value: Some(false),
5676 style: None,
5677 explode: None,
5678 allow_reserved: Some(false),
5679 schema: Some(ObjectOrReference::Ref {
5680 ref_path: "#/components/schemas/Pet".to_string(),
5681 summary: Some("Pet Reference".to_string()),
5682 description: Some("A reference to pet schema with additional context".to_string()),
5683 }),
5684 example: None,
5685 examples: BTreeMap::new(),
5686 content: None,
5687 extensions: Default::default(),
5688 };
5689
5690 let result = ToolGenerator::convert_parameter_schema(
5692 ¶m_with_ref,
5693 ParameterIn::Query,
5694 &spec,
5695 false,
5696 false,
5697 );
5698
5699 assert!(result.is_ok());
5700 let (schema, _annotations) = result.unwrap();
5701
5702 let description = schema.get("description").and_then(|v| v.as_str());
5704 assert!(description.is_some());
5705 assert!(
5707 description.unwrap().contains("Pet Reference")
5708 || description
5709 .unwrap()
5710 .contains("A reference to pet schema with additional context")
5711 );
5712 }
5713
5714 #[test]
5715 fn test_request_body_with_reference_metadata() {
5716 let spec = create_test_spec();
5717
5718 let request_body_ref = ObjectOrReference::Ref {
5720 ref_path: "#/components/requestBodies/PetBody".to_string(),
5721 summary: Some("Pet Request Body".to_string()),
5722 description: Some(
5723 "Request body containing pet information for API operations".to_string(),
5724 ),
5725 };
5726
5727 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body_ref, &spec);
5728
5729 assert!(result.is_ok());
5730 let schema_result = result.unwrap();
5731 assert!(schema_result.is_some());
5732
5733 let (schema, _annotations, _required) = schema_result.unwrap();
5734 let description = schema.get("description").and_then(|v| v.as_str());
5735
5736 assert!(description.is_some());
5737 assert_eq!(
5739 description.unwrap(),
5740 "Request body containing pet information for API operations"
5741 );
5742 }
5743
5744 #[test]
5745 fn test_response_schema_with_reference_metadata() {
5746 let spec = create_test_spec();
5747
5748 let mut responses = BTreeMap::new();
5750 responses.insert(
5751 "200".to_string(),
5752 ObjectOrReference::Ref {
5753 ref_path: "#/components/responses/PetResponse".to_string(),
5754 summary: Some("Successful Pet Response".to_string()),
5755 description: Some(
5756 "Response containing pet data on successful operation".to_string(),
5757 ),
5758 },
5759 );
5760 let responses_option = Some(responses);
5761
5762 let result = ToolGenerator::extract_output_schema(&responses_option, &spec);
5763
5764 assert!(result.is_ok());
5765 let schema = result.unwrap();
5766 assert!(schema.is_some());
5767
5768 let schema_value = schema.unwrap();
5769 let body_desc = schema_value
5770 .get("properties")
5771 .and_then(|props| props.get("body"))
5772 .and_then(|body| body.get("description"))
5773 .and_then(|desc| desc.as_str());
5774
5775 assert!(body_desc.is_some());
5776 assert_eq!(
5778 body_desc.unwrap(),
5779 "Response containing pet data on successful operation"
5780 );
5781 }
5782
5783 #[test]
5784 fn test_self_referencing_schema_does_not_overflow() {
5785 let mut spec = create_test_spec();
5788
5789 let node_schema = ObjectSchema {
5791 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5792 properties: {
5793 let mut props = BTreeMap::new();
5794 props.insert(
5795 "name".to_string(),
5796 ObjectOrReference::Object(ObjectSchema {
5797 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5798 ..Default::default()
5799 }),
5800 );
5801 props.insert(
5803 "children".to_string(),
5804 ObjectOrReference::Object(ObjectSchema {
5805 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
5806 items: Some(Box::new(Schema::Object(Box::new(ObjectOrReference::Ref {
5807 ref_path: "#/components/schemas/Node".to_string(),
5808 summary: None,
5809 description: None,
5810 })))),
5811 ..Default::default()
5812 }),
5813 );
5814 props
5815 },
5816 ..Default::default()
5817 };
5818
5819 if let Some(ref mut components) = spec.components {
5821 components
5822 .schemas
5823 .insert("Node".to_string(), ObjectOrReference::Object(node_schema));
5824 }
5825
5826 let mut visited = HashSet::new();
5828 let result = ToolGenerator::convert_schema_to_json_schema(
5829 &Schema::Object(Box::new(ObjectOrReference::Ref {
5830 ref_path: "#/components/schemas/Node".to_string(),
5831 summary: None,
5832 description: None,
5833 })),
5834 &spec,
5835 &mut visited,
5836 );
5837
5838 assert!(
5840 result.is_err(),
5841 "Expected circular reference error, got: {result:?}"
5842 );
5843 let error = result.unwrap_err();
5844 assert!(
5845 error.to_string().contains("Circular reference"),
5846 "Expected circular reference error message, got: {error}"
5847 );
5848 }
5849
5850 #[test]
5851 fn test_one_of_diamond_through_alias_is_not_circular() {
5852 let mut spec = create_test_spec();
5858 if let Some(ref mut components) = spec.components {
5859 components.schemas.insert(
5860 "Target".to_string(),
5861 ObjectOrReference::Object(ObjectSchema {
5862 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5863 ..Default::default()
5864 }),
5865 );
5866 components.schemas.insert(
5867 "AliasA".to_string(),
5868 ObjectOrReference::Ref {
5869 ref_path: "#/components/schemas/Target".to_string(),
5870 summary: None,
5871 description: None,
5872 },
5873 );
5874 }
5875 let outer = ObjectSchema {
5876 one_of: vec![
5877 ObjectOrReference::Ref {
5878 ref_path: "#/components/schemas/AliasA".to_string(),
5879 summary: None,
5880 description: None,
5881 },
5882 ObjectOrReference::Ref {
5883 ref_path: "#/components/schemas/Target".to_string(),
5884 summary: None,
5885 description: None,
5886 },
5887 ],
5888 ..Default::default()
5889 };
5890 let mut visited = HashSet::new();
5891 let result =
5892 ToolGenerator::convert_object_schema_to_json_schema(&outer, &spec, &mut visited)
5893 .expect("a DAG diamond through an alias chain is not a cycle");
5894 let branches = result["oneOf"].as_array().expect("oneOf array");
5895 assert_eq!(branches.len(), 2);
5896 assert!(branches.iter().all(|b| b["type"] == json!("string")));
5897 }
5898
5899 #[test]
5900 fn test_properties_diamond_through_alias_is_not_circular() {
5901 let mut spec = create_test_spec();
5904 if let Some(ref mut components) = spec.components {
5905 components.schemas.insert(
5906 "Target".to_string(),
5907 ObjectOrReference::Object(ObjectSchema {
5908 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5909 ..Default::default()
5910 }),
5911 );
5912 components.schemas.insert(
5913 "AliasA".to_string(),
5914 ObjectOrReference::Ref {
5915 ref_path: "#/components/schemas/Target".to_string(),
5916 summary: None,
5917 description: None,
5918 },
5919 );
5920 }
5921 let outer = ObjectSchema {
5922 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5923 properties: BTreeMap::from([
5924 (
5925 "a".to_string(),
5926 ObjectOrReference::Ref {
5927 ref_path: "#/components/schemas/AliasA".to_string(),
5928 summary: None,
5929 description: None,
5930 },
5931 ),
5932 (
5933 "b".to_string(),
5934 ObjectOrReference::Ref {
5935 ref_path: "#/components/schemas/Target".to_string(),
5936 summary: None,
5937 description: None,
5938 },
5939 ),
5940 ]),
5941 ..Default::default()
5942 };
5943 let mut visited = HashSet::new();
5944 let result =
5945 ToolGenerator::convert_object_schema_to_json_schema(&outer, &spec, &mut visited)
5946 .expect("sibling properties sharing a target via an alias are not a cycle");
5947 assert_eq!(result["properties"]["a"]["type"], json!("string"));
5948 assert_eq!(result["properties"]["b"]["type"], json!("string"));
5949 }
5950
5951 #[test]
5954 fn test_all_of_branches_are_merged_not_emptied() {
5955 let mut spec = create_test_spec();
5961
5962 let application = ObjectSchema {
5963 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5964 required: vec!["title".to_string()],
5965 properties: {
5966 let mut props = BTreeMap::new();
5967 props.insert(
5968 "title".to_string(),
5969 ObjectOrReference::Object(ObjectSchema {
5970 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5971 ..Default::default()
5972 }),
5973 );
5974 props
5975 },
5976 ..Default::default()
5977 };
5978
5979 let discriminator = ObjectSchema {
5981 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5982 required: vec!["type".to_string()],
5983 properties: {
5984 let mut props = BTreeMap::new();
5985 props.insert(
5986 "type".to_string(),
5987 ObjectOrReference::Object(ObjectSchema {
5988 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5989 enum_values: vec![json!("Application")],
5990 ..Default::default()
5991 }),
5992 );
5993 props
5994 },
5995 ..Default::default()
5996 };
5997
5998 let section = ObjectSchema {
5999 one_of: vec![ObjectOrReference::Object(ObjectSchema {
6000 all_of: vec![
6001 ObjectOrReference::Ref {
6002 ref_path: "#/components/schemas/Application".to_string(),
6003 summary: None,
6004 description: None,
6005 },
6006 ObjectOrReference::Object(discriminator),
6007 ],
6008 ..Default::default()
6009 })],
6010 ..Default::default()
6011 };
6012
6013 if let Some(ref mut components) = spec.components {
6014 components.schemas.insert(
6015 "Application".to_string(),
6016 ObjectOrReference::Object(application),
6017 );
6018 }
6019
6020 let mut visited = HashSet::new();
6021 let result =
6022 ToolGenerator::convert_object_schema_to_json_schema(§ion, &spec, &mut visited)
6023 .expect("conversion should succeed");
6024
6025 let branch = &result["oneOf"][0];
6026 assert!(
6028 branch.as_object().is_some_and(|object| !object.is_empty()),
6029 "allOf branch collapsed to empty schema: {result}"
6030 );
6031 assert_eq!(
6033 branch["properties"]["type"]["enum"][0],
6034 json!("Application")
6035 );
6036 assert!(
6037 branch["properties"].get("title").is_some(),
6038 "merged branch is missing the variant's fields: {branch}"
6039 );
6040 let required = branch["required"].as_array().expect("required array");
6042 assert!(required.iter().any(|value| value == "type"));
6043 assert!(required.iter().any(|value| value == "title"));
6044 }
6045
6046 #[test]
6047 fn test_any_of_is_surfaced() {
6048 let spec = create_test_spec();
6049 let schema = ObjectSchema {
6050 any_of: vec![
6051 ObjectOrReference::Object(ObjectSchema {
6052 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
6053 ..Default::default()
6054 }),
6055 ObjectOrReference::Object(ObjectSchema {
6056 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
6057 ..Default::default()
6058 }),
6059 ],
6060 ..Default::default()
6061 };
6062
6063 let mut visited = HashSet::new();
6064 let result =
6065 ToolGenerator::convert_object_schema_to_json_schema(&schema, &spec, &mut visited)
6066 .expect("conversion should succeed");
6067 let any_of = result["anyOf"].as_array().expect("anyOf array");
6068 assert_eq!(any_of.len(), 2);
6069 assert_eq!(any_of[0]["type"], json!("string"));
6070 assert_eq!(any_of[1]["type"], json!("integer"));
6071 }
6072
6073 #[test]
6076 fn test_multipart_form_data_with_single_file() {
6077 let request_body = ObjectOrReference::Object(RequestBody {
6080 description: Some("File upload request".to_string()),
6081 content: {
6082 let mut content = BTreeMap::new();
6083 content.insert(
6084 "multipart/form-data".to_string(),
6085 MediaType {
6086 extensions: Default::default(),
6087 schema: Some(ObjectOrReference::Object(ObjectSchema {
6088 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6089 properties: {
6090 let mut props = BTreeMap::new();
6091 props.insert(
6092 "file".to_string(),
6093 ObjectOrReference::Object(ObjectSchema {
6094 schema_type: Some(SchemaTypeSet::Single(
6095 SchemaType::String,
6096 )),
6097 format: Some("binary".to_string()),
6098 description: Some("The file to upload".to_string()),
6099 ..Default::default()
6100 }),
6101 );
6102 props
6103 },
6104 required: vec!["file".to_string()],
6105 ..Default::default()
6106 })),
6107 examples: None,
6108 encoding: Default::default(),
6109 },
6110 );
6111 content
6112 },
6113 required: Some(true),
6114 });
6115
6116 let spec = create_test_spec();
6117 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6118 .unwrap()
6119 .unwrap();
6120
6121 let (schema, annotations, is_required) = result;
6122
6123 let schema_obj = schema.as_object().unwrap();
6125 assert_eq!(schema_obj.get("type").unwrap(), "object");
6126
6127 let file_schema = schema_obj.get("properties").unwrap().get("file").unwrap();
6129
6130 assert_eq!(file_schema.get("type").unwrap(), "object");
6132 assert!(
6133 file_schema
6134 .get("properties")
6135 .unwrap()
6136 .get("content")
6137 .is_some()
6138 );
6139 assert!(
6140 file_schema
6141 .get("properties")
6142 .unwrap()
6143 .get("filename")
6144 .is_some()
6145 );
6146 assert!(
6147 file_schema
6148 .get("required")
6149 .unwrap()
6150 .as_array()
6151 .unwrap()
6152 .contains(&json!("content"))
6153 );
6154
6155 let annotations_value = serde_json::to_value(&annotations).unwrap();
6157 let annotations_obj = annotations_value.as_object().unwrap();
6158
6159 assert_eq!(
6161 annotations_obj.get("x-content-type").unwrap(),
6162 "multipart/form-data"
6163 );
6164
6165 let x_file_fields = annotations_obj
6167 .get("x-file-fields")
6168 .unwrap()
6169 .as_array()
6170 .unwrap();
6171 assert_eq!(x_file_fields.len(), 1);
6172 assert!(x_file_fields.contains(&json!("file")));
6173
6174 assert!(is_required);
6176
6177 insta::assert_json_snapshot!("test_multipart_form_data_with_single_file", schema);
6179 }
6180
6181 #[test]
6182 fn test_multipart_form_data_with_multiple_files() {
6183 let request_body = ObjectOrReference::Object(RequestBody {
6185 description: Some("Multiple file upload request".to_string()),
6186 content: {
6187 let mut content = BTreeMap::new();
6188 content.insert(
6189 "multipart/form-data".to_string(),
6190 MediaType {
6191 extensions: Default::default(),
6192 schema: Some(ObjectOrReference::Object(ObjectSchema {
6193 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6194 properties: {
6195 let mut props = BTreeMap::new();
6196 props.insert(
6197 "avatar".to_string(),
6198 ObjectOrReference::Object(ObjectSchema {
6199 schema_type: Some(SchemaTypeSet::Single(
6200 SchemaType::String,
6201 )),
6202 format: Some("binary".to_string()),
6203 description: Some("Profile avatar image".to_string()),
6204 ..Default::default()
6205 }),
6206 );
6207 props.insert(
6208 "document".to_string(),
6209 ObjectOrReference::Object(ObjectSchema {
6210 schema_type: Some(SchemaTypeSet::Single(
6211 SchemaType::String,
6212 )),
6213 format: Some("binary".to_string()),
6214 description: Some("Supporting document".to_string()),
6215 ..Default::default()
6216 }),
6217 );
6218 props.insert(
6219 "resume".to_string(),
6220 ObjectOrReference::Object(ObjectSchema {
6221 schema_type: Some(SchemaTypeSet::Single(
6222 SchemaType::String,
6223 )),
6224 format: Some("binary".to_string()),
6225 description: Some("Resume file".to_string()),
6226 ..Default::default()
6227 }),
6228 );
6229 props
6230 },
6231 required: vec!["avatar".to_string(), "resume".to_string()],
6232 ..Default::default()
6233 })),
6234 examples: None,
6235 encoding: Default::default(),
6236 },
6237 );
6238 content
6239 },
6240 required: Some(true),
6241 });
6242
6243 let spec = create_test_spec();
6244 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6245 .unwrap()
6246 .unwrap();
6247
6248 let (schema, annotations, _is_required) = result;
6249
6250 let body_properties = schema.get("properties").unwrap();
6252 for field_name in ["avatar", "document", "resume"] {
6253 let field_schema = body_properties.get(field_name).unwrap();
6254 assert_eq!(
6255 field_schema.get("type").unwrap(),
6256 "object",
6257 "Field {field_name} should be transformed to object type"
6258 );
6259 assert!(
6260 field_schema
6261 .get("properties")
6262 .unwrap()
6263 .get("content")
6264 .is_some(),
6265 "Field {field_name} should have content property"
6266 );
6267 }
6268
6269 let annotations_value = serde_json::to_value(&annotations).unwrap();
6271 let annotations_obj = annotations_value.as_object().unwrap();
6272
6273 let x_file_fields = annotations_obj
6274 .get("x-file-fields")
6275 .unwrap()
6276 .as_array()
6277 .unwrap();
6278 assert_eq!(x_file_fields.len(), 3);
6279 assert!(x_file_fields.contains(&json!("avatar")));
6280 assert!(x_file_fields.contains(&json!("document")));
6281 assert!(x_file_fields.contains(&json!("resume")));
6282
6283 insta::assert_json_snapshot!("test_multipart_form_data_with_multiple_files", schema);
6285 }
6286
6287 #[test]
6288 fn test_multipart_form_data_mixed_fields() {
6289 let request_body = ObjectOrReference::Object(RequestBody {
6291 description: Some("Profile creation with file upload".to_string()),
6292 content: {
6293 let mut content = BTreeMap::new();
6294 content.insert(
6295 "multipart/form-data".to_string(),
6296 MediaType {
6297 extensions: Default::default(),
6298 schema: Some(ObjectOrReference::Object(ObjectSchema {
6299 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6300 properties: {
6301 let mut props = BTreeMap::new();
6302 props.insert(
6304 "avatar".to_string(),
6305 ObjectOrReference::Object(ObjectSchema {
6306 schema_type: Some(SchemaTypeSet::Single(
6307 SchemaType::String,
6308 )),
6309 format: Some("binary".to_string()),
6310 description: Some("Profile avatar image".to_string()),
6311 ..Default::default()
6312 }),
6313 );
6314 props.insert(
6316 "name".to_string(),
6317 ObjectOrReference::Object(ObjectSchema {
6318 schema_type: Some(SchemaTypeSet::Single(
6319 SchemaType::String,
6320 )),
6321 description: Some("User's display name".to_string()),
6322 ..Default::default()
6323 }),
6324 );
6325 props.insert(
6327 "age".to_string(),
6328 ObjectOrReference::Object(ObjectSchema {
6329 schema_type: Some(SchemaTypeSet::Single(
6330 SchemaType::Integer,
6331 )),
6332 description: Some("User's age".to_string()),
6333 ..Default::default()
6334 }),
6335 );
6336 props.insert(
6338 "email".to_string(),
6339 ObjectOrReference::Object(ObjectSchema {
6340 schema_type: Some(SchemaTypeSet::Single(
6341 SchemaType::String,
6342 )),
6343 format: Some("email".to_string()),
6344 description: Some("User's email address".to_string()),
6345 ..Default::default()
6346 }),
6347 );
6348 props
6349 },
6350 required: vec!["name".to_string(), "avatar".to_string()],
6351 ..Default::default()
6352 })),
6353 examples: None,
6354 encoding: Default::default(),
6355 },
6356 );
6357 content
6358 },
6359 required: Some(true),
6360 });
6361
6362 let spec = create_test_spec();
6363 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6364 .unwrap()
6365 .unwrap();
6366
6367 let (schema, annotations, _is_required) = result;
6368 let body_properties = schema.get("properties").unwrap();
6369
6370 let avatar_schema = body_properties.get("avatar").unwrap();
6372 assert_eq!(avatar_schema.get("type").unwrap(), "object");
6373 assert!(
6374 avatar_schema
6375 .get("properties")
6376 .unwrap()
6377 .get("content")
6378 .is_some()
6379 );
6380 assert!(
6381 avatar_schema
6382 .get("properties")
6383 .unwrap()
6384 .get("filename")
6385 .is_some()
6386 );
6387
6388 let name_schema = body_properties.get("name").unwrap();
6390 assert_eq!(name_schema.get("type").unwrap(), "string");
6391 assert!(name_schema.get("properties").is_none()); let age_schema = body_properties.get("age").unwrap();
6395 assert_eq!(age_schema.get("type").unwrap(), "integer");
6396
6397 let email_schema = body_properties.get("email").unwrap();
6399 assert_eq!(email_schema.get("type").unwrap(), "string");
6400 assert_eq!(email_schema.get("format").unwrap(), "email");
6401
6402 let annotations_value = serde_json::to_value(&annotations).unwrap();
6404 let annotations_obj = annotations_value.as_object().unwrap();
6405
6406 let x_file_fields = annotations_obj
6407 .get("x-file-fields")
6408 .unwrap()
6409 .as_array()
6410 .unwrap();
6411 assert_eq!(x_file_fields.len(), 1);
6412 assert!(x_file_fields.contains(&json!("avatar")));
6413
6414 insta::assert_json_snapshot!("test_multipart_form_data_mixed_fields", schema);
6416 }
6417
6418 #[test]
6419 fn test_multipart_format_byte_detection() {
6420 let request_body = ObjectOrReference::Object(RequestBody {
6422 description: Some("Base64 encoded file upload".to_string()),
6423 content: {
6424 let mut content = BTreeMap::new();
6425 content.insert(
6426 "multipart/form-data".to_string(),
6427 MediaType {
6428 extensions: Default::default(),
6429 schema: Some(ObjectOrReference::Object(ObjectSchema {
6430 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6431 properties: {
6432 let mut props = BTreeMap::new();
6433 props.insert(
6435 "data".to_string(),
6436 ObjectOrReference::Object(ObjectSchema {
6437 schema_type: Some(SchemaTypeSet::Single(
6438 SchemaType::String,
6439 )),
6440 format: Some("byte".to_string()),
6441 description: Some(
6442 "Base64 encoded file content".to_string(),
6443 ),
6444 ..Default::default()
6445 }),
6446 );
6447 props.insert(
6449 "attachment".to_string(),
6450 ObjectOrReference::Object(ObjectSchema {
6451 schema_type: Some(SchemaTypeSet::Single(
6452 SchemaType::String,
6453 )),
6454 format: Some("binary".to_string()),
6455 description: Some("Binary file attachment".to_string()),
6456 ..Default::default()
6457 }),
6458 );
6459 props
6460 },
6461 required: vec!["data".to_string()],
6462 ..Default::default()
6463 })),
6464 examples: None,
6465 encoding: Default::default(),
6466 },
6467 );
6468 content
6469 },
6470 required: Some(true),
6471 });
6472
6473 let spec = create_test_spec();
6474 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6475 .unwrap()
6476 .unwrap();
6477
6478 let (schema, annotations, _is_required) = result;
6479 let body_properties = schema.get("properties").unwrap();
6480
6481 let data_schema = body_properties.get("data").unwrap();
6483 assert_eq!(data_schema.get("type").unwrap(), "object");
6484 assert!(
6485 data_schema
6486 .get("properties")
6487 .unwrap()
6488 .get("content")
6489 .is_some()
6490 );
6491
6492 let attachment_schema = body_properties.get("attachment").unwrap();
6493 assert_eq!(attachment_schema.get("type").unwrap(), "object");
6494 assert!(
6495 attachment_schema
6496 .get("properties")
6497 .unwrap()
6498 .get("content")
6499 .is_some()
6500 );
6501
6502 let annotations_value = serde_json::to_value(&annotations).unwrap();
6504 let annotations_obj = annotations_value.as_object().unwrap();
6505
6506 let x_file_fields = annotations_obj
6507 .get("x-file-fields")
6508 .unwrap()
6509 .as_array()
6510 .unwrap();
6511 assert_eq!(x_file_fields.len(), 2);
6512 assert!(x_file_fields.contains(&json!("data")));
6513 assert!(x_file_fields.contains(&json!("attachment")));
6514
6515 insta::assert_json_snapshot!("test_multipart_format_byte_detection", schema);
6517 }
6518
6519 #[test]
6520 fn test_multipart_non_file_fields_unchanged() {
6521 let request_body = ObjectOrReference::Object(RequestBody {
6523 description: Some("Form submission".to_string()),
6524 content: {
6525 let mut content = BTreeMap::new();
6526 content.insert(
6527 "multipart/form-data".to_string(),
6528 MediaType {
6529 extensions: Default::default(),
6530 schema: Some(ObjectOrReference::Object(ObjectSchema {
6531 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6532 properties: {
6533 let mut props = BTreeMap::new();
6534 props.insert(
6536 "title".to_string(),
6537 ObjectOrReference::Object(ObjectSchema {
6538 schema_type: Some(SchemaTypeSet::Single(
6539 SchemaType::String,
6540 )),
6541 description: Some("Form title".to_string()),
6542 ..Default::default()
6543 }),
6544 );
6545 props.insert(
6546 "count".to_string(),
6547 ObjectOrReference::Object(ObjectSchema {
6548 schema_type: Some(SchemaTypeSet::Single(
6549 SchemaType::Integer,
6550 )),
6551 description: Some("Item count".to_string()),
6552 ..Default::default()
6553 }),
6554 );
6555 props.insert(
6556 "enabled".to_string(),
6557 ObjectOrReference::Object(ObjectSchema {
6558 schema_type: Some(SchemaTypeSet::Single(
6559 SchemaType::Boolean,
6560 )),
6561 description: Some("Enable flag".to_string()),
6562 ..Default::default()
6563 }),
6564 );
6565 props.insert(
6566 "price".to_string(),
6567 ObjectOrReference::Object(ObjectSchema {
6568 schema_type: Some(SchemaTypeSet::Single(
6569 SchemaType::Number,
6570 )),
6571 description: Some("Price value".to_string()),
6572 ..Default::default()
6573 }),
6574 );
6575 props.insert(
6576 "uuid".to_string(),
6577 ObjectOrReference::Object(ObjectSchema {
6578 schema_type: Some(SchemaTypeSet::Single(
6579 SchemaType::String,
6580 )),
6581 format: Some("uuid".to_string()),
6582 description: Some("UUID field".to_string()),
6583 ..Default::default()
6584 }),
6585 );
6586 props.insert(
6587 "date".to_string(),
6588 ObjectOrReference::Object(ObjectSchema {
6589 schema_type: Some(SchemaTypeSet::Single(
6590 SchemaType::String,
6591 )),
6592 format: Some("date".to_string()),
6593 description: Some("Date field".to_string()),
6594 ..Default::default()
6595 }),
6596 );
6597 props
6598 },
6599 required: vec!["title".to_string()],
6600 ..Default::default()
6601 })),
6602 examples: None,
6603 encoding: Default::default(),
6604 },
6605 );
6606 content
6607 },
6608 required: Some(true),
6609 });
6610
6611 let spec = create_test_spec();
6612 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6613 .unwrap()
6614 .unwrap();
6615
6616 let (schema, annotations, _is_required) = result;
6617 let body_properties = schema.get("properties").unwrap();
6618
6619 let title_schema = body_properties.get("title").unwrap();
6621 assert_eq!(title_schema.get("type").unwrap(), "string");
6622 assert!(title_schema.get("properties").is_none());
6623
6624 let count_schema = body_properties.get("count").unwrap();
6626 assert_eq!(count_schema.get("type").unwrap(), "integer");
6627
6628 let enabled_schema = body_properties.get("enabled").unwrap();
6630 assert_eq!(enabled_schema.get("type").unwrap(), "boolean");
6631
6632 let price_schema = body_properties.get("price").unwrap();
6634 assert_eq!(price_schema.get("type").unwrap(), "number");
6635
6636 let uuid_schema = body_properties.get("uuid").unwrap();
6638 assert_eq!(uuid_schema.get("type").unwrap(), "string");
6639 assert_eq!(uuid_schema.get("format").unwrap(), "uuid");
6640
6641 let date_schema = body_properties.get("date").unwrap();
6643 assert_eq!(date_schema.get("type").unwrap(), "string");
6644 assert_eq!(date_schema.get("format").unwrap(), "date");
6645
6646 let annotations_value = serde_json::to_value(&annotations).unwrap();
6648 let annotations_obj = annotations_value.as_object().unwrap();
6649
6650 assert!(
6651 annotations_obj.get("x-file-fields").is_none(),
6652 "x-file-fields should not be present when there are no file fields"
6653 );
6654
6655 assert_eq!(
6657 annotations_obj.get("x-content-type").unwrap(),
6658 "multipart/form-data"
6659 );
6660
6661 insta::assert_json_snapshot!("test_multipart_non_file_fields_unchanged", schema);
6663 }
6664}