use crank_core::HttpMethod; use serde_json::Value; use crate::rest::{ model::{ ImportFinding, RestImportDocument, RestImportOperation, RestImportParameter, RestParameterLocation, }, normalize::{ImportParseError, resolve_local_ref}, recommendations::document_finding, }; pub fn parse_document(root: &Value) -> Result { let version = root .get("openapi") .and_then(Value::as_str) .map(ToOwned::to_owned); let title = root .pointer("/info/title") .and_then(Value::as_str) .unwrap_or("Imported API") .to_owned(); let servers = root .get("servers") .and_then(Value::as_array) .map(|items| { items .iter() .filter_map(|item| item.get("url").and_then(Value::as_str)) .map(|url| url.trim_end_matches('/').to_owned()) .collect::>() }) .unwrap_or_default(); let mut findings = Vec::new(); if servers.is_empty() { findings.push(document_finding( "missing_servers", "В документе не указаны servers, base URL нужно будет выбрать вручную.", )); } else if servers.len() > 1 { findings.push(document_finding( "multiple_servers", "В документе несколько servers, при импорте нужно выбрать нужный base URL.", )); } let mut operations = Vec::new(); let paths = root .get("paths") .and_then(Value::as_object) .ok_or(ImportParseError::UnsupportedDocument)?; for (path, path_item) in paths { let path_parameters = parameters(root, path_item.get("parameters")); for method_name in ["get", "post", "put", "patch", "delete"] { let Some(operation_value) = path_item.get(method_name) else { continue; }; let Some(method) = method_from_lower(method_name) else { continue; }; let mut operation_parameters = path_parameters.clone(); operation_parameters.extend(parameters(root, operation_value.get("parameters"))); let operation_servers = operation_value .get("servers") .and_then(Value::as_array) .map(|items| { items .iter() .filter_map(|item| item.get("url").and_then(Value::as_str)) .map(|url| url.trim_end_matches('/').to_owned()) .collect::>() }) .unwrap_or_default(); operations.push(RestImportOperation { key: format!("{} {}", method_name.to_uppercase(), path), method, path: path.clone(), operation_id: operation_value .get("operationId") .and_then(Value::as_str) .map(ToOwned::to_owned), summary: operation_value .get("summary") .and_then(Value::as_str) .map(ToOwned::to_owned), description: operation_value .get("description") .and_then(Value::as_str) .map(ToOwned::to_owned), tags: operation_value .get("tags") .and_then(Value::as_array) .map(|items| { items .iter() .filter_map(Value::as_str) .map(ToOwned::to_owned) .collect() }) .unwrap_or_default(), parameters: operation_parameters, request_body_schema: request_body_schema(root, operation_value), response_schema: response_schema(root, operation_value), servers: operation_servers, findings: operation_findings(operation_value), }); } } Ok(RestImportDocument { format: "openapi".to_owned(), version, title, servers, operations, findings, }) } fn parameters(root: &Value, value: Option<&Value>) -> Vec { value .and_then(Value::as_array) .map(|items| { items .iter() .filter_map(|item| { let item = resolve_local_ref(root, item, 0); let location = match item.get("in").and_then(Value::as_str)? { "path" => RestParameterLocation::Path, "query" => RestParameterLocation::Query, "header" => RestParameterLocation::Header, _ => return None, }; Some(RestImportParameter { name: item.get("name").and_then(Value::as_str)?.to_owned(), location, required: item .get("required") .and_then(Value::as_bool) .unwrap_or(false) || location == RestParameterLocation::Path, description: item .get("description") .and_then(Value::as_str) .map(ToOwned::to_owned), schema: item .get("schema") .map(|schema| resolve_local_ref(root, schema, 0)), }) }) .collect() }) .unwrap_or_default() } fn request_body_schema(root: &Value, operation: &Value) -> Option { let body = resolve_local_ref(root, operation.get("requestBody")?, 0); let content = body.get("content")?.as_object()?; for content_type in ["application/json", "application/*+json"] { if let Some(schema) = content .get(content_type) .and_then(|media| media.get("schema")) { return Some(resolve_local_ref(root, schema, 0)); } } content .iter() .find(|(content_type, _)| content_type.contains("json")) .and_then(|(_, media)| media.get("schema")) .map(|schema| resolve_local_ref(root, schema, 0)) } fn response_schema(root: &Value, operation: &Value) -> Option { let responses = operation.get("responses")?.as_object()?; for code in ["200", "201", "202", "default"] { let Some(response) = responses.get(code) else { continue; }; let response = resolve_local_ref(root, response, 0); let Some(content) = response.get("content").and_then(Value::as_object) else { continue; }; for content_type in ["application/json", "application/*+json"] { if let Some(schema) = content .get(content_type) .and_then(|media| media.get("schema")) { return Some(resolve_local_ref(root, schema, 0)); } } if let Some(schema) = content .iter() .find(|(content_type, _)| content_type.contains("json")) .and_then(|(_, media)| media.get("schema")) { return Some(resolve_local_ref(root, schema, 0)); } } None } fn operation_findings(operation: &Value) -> Vec { let mut findings = Vec::new(); if operation.get("requestBody").is_some() && request_body_schema(&Value::Null, operation).is_none() { findings.push(document_finding( "unsupported_request_body", "У метода есть requestBody, но JSON schema не найдена.", )); } findings } fn method_from_lower(value: &str) -> Option { match value { "get" => Some(HttpMethod::Get), "post" => Some(HttpMethod::Post), "put" => Some(HttpMethod::Put), "patch" => Some(HttpMethod::Patch), "delete" => Some(HttpMethod::Delete), _ => None, } }