use std::collections::{BTreeMap, BTreeSet}; use crank_core::{ ExecutionConfig, HttpMethod, OperationSafetyClass, OperationSafetyPolicy, Protocol, RestTarget, ToolDescription, ToolExample, WizardState, }; use serde_json::json; use crate::rest::{ mapping::{BodyFieldMapping, input_mapping, output_mapping}, model::{ ImportOperationCandidate, RestImportCandidate, RestImportOperation, RestParameterLocation, }, naming::{fallback_operation_seed, title_from_name, unique_name}, recommendations::{candidate_recommendations, operation_recommendations}, schema::{any_object, field_count, object_with_fields, schema_from_openapi}, }; pub fn candidate_from_operation( operation: &RestImportOperation, document_servers: &[String], used_names: &mut BTreeSet, ) -> ImportOperationCandidate { let method_name = method_as_str(operation.method); let seed = operation .operation_id .as_deref() .filter(|value| !value.trim().is_empty()) .map(ToOwned::to_owned) .unwrap_or_else(|| fallback_operation_seed(method_name, &operation.path)); let name = unique_name(&seed, used_names); let display_name = operation .summary .as_deref() .filter(|value| !value.trim().is_empty()) .map(ToOwned::to_owned) .unwrap_or_else(|| title_from_name(&name)); let description = operation .description .as_deref() .or(operation.summary.as_deref()) .filter(|value| !value.trim().is_empty()) .map(ToOwned::to_owned) .unwrap_or_else(|| format!("Выполняет {method_name} {}", operation.path)); let category = operation .tags .first() .cloned() .unwrap_or_else(|| "imported".to_owned()); let server_urls = if operation.servers.is_empty() { document_servers.to_vec() } else { operation.servers.clone() }; let base_url = server_urls.first().cloned().unwrap_or_default(); let mut input_fields = BTreeMap::new(); let mut used_input_field_names = BTreeSet::new(); for parameter in &operation.parameters { used_input_field_names.insert(parameter.name.clone()); input_fields.insert( parameter.name.clone(), schema_from_openapi( parameter.schema.as_ref(), parameter.required || parameter.location == RestParameterLocation::Path, parameter.description.clone(), ), ); } let body_fields = body_input_fields( operation.request_body_schema.as_ref(), &mut used_input_field_names, &mut input_fields, ); let input_schema = object_with_fields( Some("Входные параметры MCP-инструмента".to_owned()), input_fields, ); let output_schema = operation .response_schema .as_ref() .map(|schema| schema_from_openapi(Some(schema), true, Some("Ответ API".to_owned()))) .unwrap_or_else(|| any_object(Some("Ответ API".to_owned()))); let input_mapping = input_mapping(&operation.parameters, &body_fields); let output_mapping = output_mapping(&output_schema); let mut findings = operation_recommendations(operation); findings.extend(candidate_recommendations( operation, &name, &description, &input_schema, &output_schema, )); let draft = RestImportCandidate { name: name.clone(), display_name: display_name.clone(), category: category.clone(), target: RestTarget { base_url, method: operation.method, path_template: operation.path.clone(), static_headers: BTreeMap::new(), }, input_schema: input_schema.clone(), output_schema: output_schema.clone(), input_mapping, output_mapping, tool_description: ToolDescription { title: display_name.clone(), description: description.clone(), tags: operation.tags.clone(), examples: vec![ToolExample { input: json!({}) }], }, wizard_state: Some(WizardState { input_sample: None, output_sample: None, test_input: None, import_findings: Vec::new(), }), }; ImportOperationCandidate { key: operation.key.clone(), method: operation.method, path: operation.path.clone(), operation_id: operation.operation_id.clone(), suggested_name: name, suggested_display_name: display_name, description, category, input_fields: field_count(&input_schema), output_fields: field_count(&output_schema), server_urls, findings, draft, } } fn body_input_fields( request_body_schema: Option<&serde_json::Value>, used_names: &mut BTreeSet, input_fields: &mut BTreeMap, ) -> Vec { let Some(request_body_schema) = request_body_schema else { return Vec::new(); }; let body_schema = schema_from_openapi( Some(request_body_schema), true, Some("Тело запроса".to_owned()), ); if body_schema.kind != crank_schema::SchemaKind::Object || body_schema.fields.is_empty() { let input_name = unique_body_input_name("body", used_names); input_fields.insert(input_name.clone(), body_schema); return vec![BodyFieldMapping { input_name, body_path: String::new(), required: true, }]; } let mut mappings = Vec::new(); for (field_name, field_schema) in body_schema.fields { let input_name = unique_body_input_name(&field_name, used_names); mappings.push(BodyFieldMapping { input_name: input_name.clone(), body_path: field_name, required: field_schema.required, }); input_fields.insert(input_name, field_schema); } mappings } fn unique_body_input_name(name: &str, used_names: &mut BTreeSet) -> String { if used_names.insert(name.to_owned()) { return name.to_owned(); } let prefixed = format!("body_{name}"); if used_names.insert(prefixed.clone()) { return prefixed; } for index in 2.. { let candidate = format!("body_{name}_{index}"); if used_names.insert(candidate.clone()) { return candidate; } } unreachable!() } pub fn operation_draft_from_candidate( candidate: &ImportOperationCandidate, server_url: Option<&str>, ) -> RestImportCandidate { let mut draft = candidate.draft.clone(); if let Some(server_url) = server_url.filter(|value| !value.trim().is_empty()) { draft.target.base_url = server_url.trim().trim_end_matches('/').to_owned(); } draft } fn method_as_str(method: HttpMethod) -> &'static str { match method { HttpMethod::Get => "GET", HttpMethod::Post => "POST", HttpMethod::Put => "PUT", HttpMethod::Patch => "PATCH", HttpMethod::Delete => "DELETE", } } #[allow(dead_code)] fn safety_for_method(method: HttpMethod) -> OperationSafetyPolicy { let class = match method { HttpMethod::Get => OperationSafetyClass::Read, HttpMethod::Post | HttpMethod::Put | HttpMethod::Patch => OperationSafetyClass::Write, HttpMethod::Delete => OperationSafetyClass::Destructive, }; OperationSafetyPolicy { class, confirmation: None, } } #[allow(dead_code)] fn default_execution_config() -> ExecutionConfig { ExecutionConfig { timeout_ms: 10_000, retry_policy: None, response_cache: None, idempotency: None, safety: None, approval_policy: None, auth_profile_ref: None, headers: BTreeMap::new(), } } #[allow(dead_code)] fn _protocol() -> Protocol { Protocol::Rest }