1use crate::error::ComposioError;
14use serde_json::{json, Value};
15use std::collections::HashMap;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum OpenApiType {
20 Null,
21 Boolean,
22 Integer,
23 Number,
24 String,
25 Array,
26 Object,
27}
28
29impl OpenApiType {
30 pub fn from_str(s: &str) -> Result<Self, ComposioError> {
32 match s {
33 "null" => Ok(Self::Null),
34 "boolean" => Ok(Self::Boolean),
35 "integer" => Ok(Self::Integer),
36 "number" => Ok(Self::Number),
37 "string" => Ok(Self::String),
38 "array" => Ok(Self::Array),
39 "object" => Ok(Self::Object),
40 _ => Err(ComposioError::InvalidSchema(format!(
41 "Unknown OpenAPI type: {}",
42 s
43 ))),
44 }
45 }
46
47 pub fn to_rust_type(&self) -> &'static str {
49 match self {
50 Self::Null => "Option<()>",
51 Self::Boolean => "bool",
52 Self::Integer => "i64",
53 Self::Number => "f64",
54 Self::String => "String",
55 Self::Array => "Vec<Value>",
56 Self::Object => "HashMap<String, Value>",
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct OpenApiSchema {
64 schema: Value,
65}
66
67impl OpenApiSchema {
68 pub fn new(schema: Value) -> Self {
70 Self { schema }
71 }
72
73 pub fn get_type(&self) -> Result<OpenApiType, ComposioError> {
75 if let Some(type_str) = self.schema.get("type").and_then(|v| v.as_str()) {
76 OpenApiType::from_str(type_str)
77 } else if self.schema.get("oneOf").is_some()
78 || self.schema.get("anyOf").is_some()
79 || self.schema.get("allOf").is_some()
80 {
81 Ok(OpenApiType::Object)
83 } else {
84 Ok(OpenApiType::Object)
86 }
87 }
88
89 pub fn is_enum(&self) -> bool {
91 self.schema.get("enum").is_some()
92 }
93
94 pub fn get_enum_values(&self) -> Option<Vec<Value>> {
96 self.schema
97 .get("enum")
98 .and_then(|v| v.as_array())
99 .map(|arr| arr.clone())
100 }
101
102 pub fn is_composite(&self) -> bool {
104 self.schema.get("oneOf").is_some()
105 || self.schema.get("anyOf").is_some()
106 || self.schema.get("allOf").is_some()
107 }
108
109 pub fn get_composite_schemas(&self) -> Option<(CompositeType, Vec<OpenApiSchema>)> {
111 if let Some(schemas) = self.schema.get("oneOf").and_then(|v| v.as_array()) {
112 return Some((
113 CompositeType::OneOf,
114 schemas.iter().map(|s| OpenApiSchema::new(s.clone())).collect(),
115 ));
116 }
117 if let Some(schemas) = self.schema.get("anyOf").and_then(|v| v.as_array()) {
118 return Some((
119 CompositeType::AnyOf,
120 schemas.iter().map(|s| OpenApiSchema::new(s.clone())).collect(),
121 ));
122 }
123 if let Some(schemas) = self.schema.get("allOf").and_then(|v| v.as_array()) {
124 return Some((
125 CompositeType::AllOf,
126 schemas.iter().map(|s| OpenApiSchema::new(s.clone())).collect(),
127 ));
128 }
129 None
130 }
131
132 pub fn get_array_items(&self) -> Option<OpenApiSchema> {
134 self.schema
135 .get("items")
136 .map(|items| OpenApiSchema::new(items.clone()))
137 }
138
139 pub fn get_properties(&self) -> HashMap<String, OpenApiSchema> {
141 self.schema
142 .get("properties")
143 .and_then(|v| v.as_object())
144 .map(|props| {
145 props
146 .iter()
147 .map(|(k, v)| (k.clone(), OpenApiSchema::new(v.clone())))
148 .collect()
149 })
150 .unwrap_or_default()
151 }
152
153 pub fn get_required(&self) -> Vec<String> {
155 self.schema
156 .get("required")
157 .and_then(|v| v.as_array())
158 .map(|arr| {
159 arr.iter()
160 .filter_map(|v| v.as_str().map(String::from))
161 .collect()
162 })
163 .unwrap_or_default()
164 }
165
166 pub fn get_default(&self) -> Option<Value> {
168 self.schema.get("default").cloned()
169 }
170
171 pub fn get_description(&self) -> Option<String> {
173 self.schema
174 .get("description")
175 .and_then(|v| v.as_str())
176 .map(String::from)
177 }
178
179 pub fn validate(&self, value: &Value) -> Result<(), ComposioError> {
181 let schema_type = self.get_type()?;
183
184 match schema_type {
185 OpenApiType::Null => {
186 if !value.is_null() {
187 return Err(ComposioError::InvalidSchema(
188 "Expected null value".to_string(),
189 ));
190 }
191 }
192 OpenApiType::Boolean => {
193 if !value.is_boolean() {
194 return Err(ComposioError::InvalidSchema(
195 "Expected boolean value".to_string(),
196 ));
197 }
198 }
199 OpenApiType::Integer => {
200 if !value.is_i64() && !value.is_u64() {
201 return Err(ComposioError::InvalidSchema(
202 "Expected integer value".to_string(),
203 ));
204 }
205 }
206 OpenApiType::Number => {
207 if !value.is_number() {
208 return Err(ComposioError::InvalidSchema(
209 "Expected number value".to_string(),
210 ));
211 }
212 }
213 OpenApiType::String => {
214 if !value.is_string() {
215 return Err(ComposioError::InvalidSchema(
216 "Expected string value".to_string(),
217 ));
218 }
219 }
220 OpenApiType::Array => {
221 if !value.is_array() {
222 return Err(ComposioError::InvalidSchema(
223 "Expected array value".to_string(),
224 ));
225 }
226 if let Some(items_schema) = self.get_array_items() {
228 if let Some(arr) = value.as_array() {
229 for item in arr {
230 items_schema.validate(item)?;
231 }
232 }
233 }
234 }
235 OpenApiType::Object => {
236 if !value.is_object() {
237 return Err(ComposioError::InvalidSchema(
238 "Expected object value".to_string(),
239 ));
240 }
241 }
242 }
243
244 if self.is_enum() {
246 if let Some(enum_values) = self.get_enum_values() {
247 if !enum_values.contains(value) {
248 return Err(ComposioError::InvalidSchema(format!(
249 "Value {:?} not in enum: {:?}",
250 value, enum_values
251 )));
252 }
253 }
254 }
255
256 Ok(())
257 }
258
259 pub fn to_rust_type_string(&self) -> String {
261 if self.is_enum() {
262 if let Some(values) = self.get_enum_values() {
263 let variants: Vec<String> = values
264 .iter()
265 .filter_map(|v| {
266 if let Some(s) = v.as_str() {
267 Some(format!("\"{}\"", s))
268 } else {
269 Some(v.to_string())
270 }
271 })
272 .collect();
273 return format!("Enum({})", variants.join(" | "));
274 }
275 }
276
277 if let Some((composite_type, schemas)) = self.get_composite_schemas() {
278 let type_strings: Vec<String> = schemas
279 .iter()
280 .map(|s| s.to_rust_type_string())
281 .collect();
282 return match composite_type {
283 CompositeType::OneOf | CompositeType::AnyOf => {
284 format!("Union({})", type_strings.join(" | "))
285 }
286 CompositeType::AllOf => {
287 format!("Intersection({})", type_strings.join(" & "))
288 }
289 };
290 }
291
292 match self.get_type() {
293 Ok(t) => t.to_rust_type().to_string(),
294 Err(_) => "Value".to_string(),
295 }
296 }
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum CompositeType {
302 OneOf,
303 AnyOf,
304 AllOf,
305}
306
307#[derive(Debug, Clone)]
309pub struct ParameterDefinition {
310 pub name: String,
312
313 pub type_description: String,
315
316 pub required: bool,
318
319 pub default: Option<Value>,
321
322 pub description: Option<String>,
324
325 pub schema: OpenApiSchema,
327}
328
329pub fn extract_parameters(schema: &Value) -> Vec<ParameterDefinition> {
342 let schema = OpenApiSchema::new(schema.clone());
343 let properties = schema.get_properties();
344 let required = schema.get_required();
345 let required_set: std::collections::HashSet<_> = required.iter().collect();
346
347 properties
348 .into_iter()
349 .map(|(name, prop_schema)| ParameterDefinition {
350 type_description: prop_schema.to_rust_type_string(),
351 required: required_set.contains(&name),
352 default: prop_schema.get_default(),
353 description: prop_schema.get_description(),
354 schema: prop_schema.clone(),
355 name,
356 })
357 .collect()
358}
359
360pub fn merge_schemas(schemas: &[Value]) -> Value {
362 let mut merged = json!({
363 "type": "object",
364 "properties": {},
365 "required": []
366 });
367
368 for schema in schemas {
369 if let Some(props) = schema.get("properties").and_then(|v| v.as_object()) {
370 if let Some(merged_props) = merged.get_mut("properties").and_then(|v| v.as_object_mut()) {
371 for (key, value) in props {
372 merged_props.insert(key.clone(), value.clone());
373 }
374 }
375 }
376
377 if let Some(req) = schema.get("required").and_then(|v| v.as_array()) {
378 if let Some(merged_req) = merged.get_mut("required").and_then(|v| v.as_array_mut()) {
379 for item in req {
380 if !merged_req.contains(item) {
381 merged_req.push(item.clone());
382 }
383 }
384 }
385 }
386 }
387
388 merged
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_openapi_type_from_str() {
397 assert_eq!(OpenApiType::from_str("string").unwrap(), OpenApiType::String);
398 assert_eq!(OpenApiType::from_str("integer").unwrap(), OpenApiType::Integer);
399 assert_eq!(OpenApiType::from_str("boolean").unwrap(), OpenApiType::Boolean);
400 assert!(OpenApiType::from_str("invalid").is_err());
401 }
402
403 #[test]
404 fn test_openapi_type_to_rust() {
405 assert_eq!(OpenApiType::String.to_rust_type(), "String");
406 assert_eq!(OpenApiType::Integer.to_rust_type(), "i64");
407 assert_eq!(OpenApiType::Boolean.to_rust_type(), "bool");
408 }
409
410 #[test]
411 fn test_schema_get_type() {
412 let schema = json!({"type": "string"});
413 let openapi_schema = OpenApiSchema::new(schema);
414 assert_eq!(openapi_schema.get_type().unwrap(), OpenApiType::String);
415 }
416
417 #[test]
418 fn test_schema_is_enum() {
419 let schema = json!({
420 "type": "string",
421 "enum": ["option1", "option2"]
422 });
423 let openapi_schema = OpenApiSchema::new(schema);
424 assert!(openapi_schema.is_enum());
425 assert_eq!(openapi_schema.get_enum_values().unwrap().len(), 2);
426 }
427
428 #[test]
429 fn test_schema_validate_string() {
430 let schema = json!({"type": "string"});
431 let openapi_schema = OpenApiSchema::new(schema);
432
433 assert!(openapi_schema.validate(&json!("hello")).is_ok());
434 assert!(openapi_schema.validate(&json!(123)).is_err());
435 }
436
437 #[test]
438 fn test_schema_validate_enum() {
439 let schema = json!({
440 "type": "string",
441 "enum": ["red", "green", "blue"]
442 });
443 let openapi_schema = OpenApiSchema::new(schema);
444
445 assert!(openapi_schema.validate(&json!("red")).is_ok());
446 assert!(openapi_schema.validate(&json!("yellow")).is_err());
447 }
448
449 #[test]
450 fn test_schema_get_properties() {
451 let schema = json!({
452 "type": "object",
453 "properties": {
454 "name": {"type": "string"},
455 "age": {"type": "integer"}
456 }
457 });
458 let openapi_schema = OpenApiSchema::new(schema);
459 let props = openapi_schema.get_properties();
460
461 assert_eq!(props.len(), 2);
462 assert!(props.contains_key("name"));
463 assert!(props.contains_key("age"));
464 }
465
466 #[test]
467 fn test_schema_get_required() {
468 let schema = json!({
469 "type": "object",
470 "properties": {
471 "name": {"type": "string"},
472 "age": {"type": "integer"}
473 },
474 "required": ["name"]
475 });
476 let openapi_schema = OpenApiSchema::new(schema);
477 let required = openapi_schema.get_required();
478
479 assert_eq!(required.len(), 1);
480 assert_eq!(required[0], "name");
481 }
482
483 #[test]
484 fn test_extract_parameters() {
485 let schema = json!({
486 "type": "object",
487 "properties": {
488 "title": {
489 "type": "string",
490 "description": "Issue title"
491 },
492 "body": {
493 "type": "string",
494 "default": ""
495 },
496 "priority": {
497 "type": "string",
498 "enum": ["low", "medium", "high"]
499 }
500 },
501 "required": ["title"]
502 });
503
504 let params = extract_parameters(&schema);
505 assert_eq!(params.len(), 3);
506
507 let title_param = params.iter().find(|p| p.name == "title").unwrap();
508 assert!(title_param.required);
509 assert_eq!(title_param.description, Some("Issue title".to_string()));
510
511 let body_param = params.iter().find(|p| p.name == "body").unwrap();
512 assert!(!body_param.required);
513 assert_eq!(body_param.default, Some(json!("")));
514 }
515
516 #[test]
517 fn test_composite_schema_oneof() {
518 let schema = json!({
519 "oneOf": [
520 {"type": "string"},
521 {"type": "integer"}
522 ]
523 });
524 let openapi_schema = OpenApiSchema::new(schema);
525
526 assert!(openapi_schema.is_composite());
527 let (composite_type, schemas) = openapi_schema.get_composite_schemas().unwrap();
528 assert_eq!(composite_type, CompositeType::OneOf);
529 assert_eq!(schemas.len(), 2);
530 }
531
532 #[test]
533 fn test_merge_schemas() {
534 let schema1 = json!({
535 "type": "object",
536 "properties": {
537 "name": {"type": "string"}
538 },
539 "required": ["name"]
540 });
541
542 let schema2 = json!({
543 "type": "object",
544 "properties": {
545 "age": {"type": "integer"}
546 },
547 "required": ["age"]
548 });
549
550 let merged = merge_schemas(&[schema1, schema2]);
551 let props = merged.get("properties").unwrap().as_object().unwrap();
552 let required = merged.get("required").unwrap().as_array().unwrap();
553
554 assert_eq!(props.len(), 2);
555 assert_eq!(required.len(), 2);
556 }
557
558 #[test]
559 fn test_array_schema() {
560 let schema = json!({
561 "type": "array",
562 "items": {"type": "string"}
563 });
564 let openapi_schema = OpenApiSchema::new(schema);
565
566 assert_eq!(openapi_schema.get_type().unwrap(), OpenApiType::Array);
567 assert!(openapi_schema.get_array_items().is_some());
568
569 assert!(openapi_schema.validate(&json!(["a", "b", "c"])).is_ok());
571 assert!(openapi_schema.validate(&json!([1, 2, 3])).is_err());
573 }
574}