use std::{collections::BTreeMap, io}; use oas3::Spec; use oas3::spec::{ObjectOrReference, Operation, ParameterIn, PathItem, Schema, SchemaTypeSet}; use serde_json::Value; /// Type schema extracted from OpenAPI components pub type TypeSchemas = BTreeMap; /// Endpoint information extracted from OpenAPI spec #[derive(Debug, Clone)] pub struct Endpoint { /// HTTP method (GET, POST, etc.) pub method: String, /// Path template (e.g., "/blocks/{hash}") pub path: String, /// Operation ID (e.g., "getBlockByHash") pub operation_id: Option, /// Short summary pub summary: Option, /// Detailed description pub description: Option, /// Tags for grouping pub tags: Vec, /// Path parameters pub path_params: Vec, /// Query parameters pub query_params: Vec, /// Response type (simplified) pub response_type: Option, /// Whether this endpoint is deprecated pub deprecated: bool, } impl Endpoint { /// Returns true if this endpoint should be included in client generation. /// Only non-deprecated GET endpoints are included. pub fn should_generate(&self) -> bool { self.method == "GET" && !self.deprecated } /// Returns the operation ID or generates one from the path. /// The returned string uses the raw case from the spec (typically camelCase). pub fn operation_name(&self) -> String { if let Some(op_id) = &self.operation_id { return op_id.clone(); } // Generate from path: /api/blocks/{hash} -> "get_api_blocks_by_hash" let parts: Vec = self .path .split('/') .filter(|s| !s.is_empty()) .map(|segment| { if let Some(param) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) { format!("by_{}", param) } else { segment.to_string() } }) .collect(); format!("get_{}", parts.join("_")) } } /// Parameter information #[derive(Debug, Clone)] pub struct Parameter { pub name: String, pub required: bool, pub param_type: String, pub description: Option, } /// Parse OpenAPI spec from JSON string /// /// Pre-processes the JSON to handle oas3 limitations: /// - Removes unsupported siblings from `$ref` objects (oas3 only supports `summary` and `description`) pub fn parse_openapi_json(json: &str) -> io::Result { let mut value: Value = serde_json::from_str(json).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; // Clean up for oas3 compatibility clean_for_oas3(&mut value); let cleaned_json = serde_json::to_string(&value).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; oas3::from_json(&cleaned_json).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) } /// Extract type schemas from OpenAPI JSON pub fn extract_schemas(json: &str) -> TypeSchemas { let Ok(value) = serde_json::from_str::(json) else { return BTreeMap::new(); }; value .get("components") .and_then(|c| c.get("schemas")) .and_then(|s| s.as_object()) .map(|schemas| { schemas .iter() .map(|(name, schema)| (name.clone(), schema.clone())) .collect() }) .unwrap_or_default() } /// Clean up OpenAPI spec for oas3 compatibility. /// - Removes unsupported siblings from $ref objects (oas3 only supports summary and description) /// - Converts boolean schemas to object schemas (oas3 doesn't handle `"schema": true`) fn clean_for_oas3(value: &mut Value) { match value { Value::Object(map) => { // Handle $ref with unsupported siblings if map.contains_key("$ref") { map.retain(|k, _| k == "$ref" || k == "summary" || k == "description"); } else { // Convert boolean schemas to empty object schemas if let Some(schema) = map.get_mut("schema") && schema.is_boolean() { *schema = Value::Object(serde_json::Map::new()); } for v in map.values_mut() { clean_for_oas3(v); } } } Value::Array(arr) => { for v in arr { clean_for_oas3(v); } } _ => {} } } /// Extract all endpoints from OpenAPI spec pub fn extract_endpoints(spec: &Spec) -> Vec { let mut endpoints = Vec::new(); let Some(paths) = &spec.paths else { return endpoints; }; for (path, path_item) in paths { for (method, operation) in get_operations(path_item) { if let Some(endpoint) = extract_endpoint(path, &method, operation) { endpoints.push(endpoint); } } } endpoints } fn get_operations(path_item: &PathItem) -> Vec<(String, &Operation)> { let mut ops = Vec::new(); if let Some(op) = &path_item.get { ops.push(("GET".to_string(), op)); } if let Some(op) = &path_item.post { ops.push(("POST".to_string(), op)); } if let Some(op) = &path_item.put { ops.push(("PUT".to_string(), op)); } if let Some(op) = &path_item.delete { ops.push(("DELETE".to_string(), op)); } if let Some(op) = &path_item.patch { ops.push(("PATCH".to_string(), op)); } ops } fn extract_endpoint(path: &str, method: &str, operation: &Operation) -> Option { let path_params = extract_parameters(operation, ParameterIn::Path); let query_params = extract_parameters(operation, ParameterIn::Query); let response_type = extract_response_type(operation); Some(Endpoint { method: method.to_string(), path: path.to_string(), operation_id: operation.operation_id.clone(), summary: operation.summary.clone(), description: operation.description.clone(), tags: operation.tags.clone(), path_params, query_params, response_type, deprecated: operation.deprecated.unwrap_or(false), }) } fn extract_parameters(operation: &Operation, location: ParameterIn) -> Vec { operation .parameters .iter() .filter_map(|p| match p { ObjectOrReference::Object(param) if param.location == location => Some(Parameter { name: param.name.clone(), required: param.required.unwrap_or(false), param_type: "string".to_string(), // Simplified description: param.description.clone(), }), _ => None, }) .collect() } fn extract_response_type(operation: &Operation) -> Option { let responses = operation.responses.as_ref()?; // Look for 200 OK response let response = responses.get("200")?; match response { ObjectOrReference::Object(response) => { // Look for JSON content let content = response.content.get("application/json")?; match &content.schema { Some(ObjectOrReference::Ref { ref_path, .. }) => { // Extract type name from reference like "#/components/schemas/Block" Some(ref_path.rsplit('/').next()?.to_string()) } Some(ObjectOrReference::Object(schema)) => schema_to_type_name(schema), None => None, } } ObjectOrReference::Ref { .. } => None, } } fn schema_type_from_schema(schema: &Schema) -> Option { match schema { Schema::Boolean(_) => Some("boolean".to_string()), Schema::Object(obj_or_ref) => match obj_or_ref.as_ref() { ObjectOrReference::Object(obj_schema) => schema_to_type_name(obj_schema), ObjectOrReference::Ref { ref_path, .. } => { ref_path.rsplit('/').next().map(|s| s.to_string()) } }, } } fn schema_to_type_name(schema: &oas3::spec::ObjectSchema) -> Option { let schema_type = schema.schema_type.as_ref()?; match schema_type { SchemaTypeSet::Single(t) => match t { oas3::spec::SchemaType::String => Some("string".to_string()), oas3::spec::SchemaType::Number => Some("number".to_string()), oas3::spec::SchemaType::Integer => Some("number".to_string()), oas3::spec::SchemaType::Boolean => Some("boolean".to_string()), oas3::spec::SchemaType::Array => { let inner = match &schema.items { Some(boxed_schema) => schema_type_from_schema(boxed_schema), None => Some("*".to_string()), }; inner.map(|t| format!("{}[]", t)) } oas3::spec::SchemaType::Object => Some("Object".to_string()), oas3::spec::SchemaType::Null => Some("null".to_string()), }, SchemaTypeSet::Multiple(_) => Some("*".to_string()), } }