Files
crank/crates/crank-import/src/rest/openapi3.rs
T
2026-06-23 21:03:03 +00:00

229 lines
8.2 KiB
Rust

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<RestImportDocument, ImportParseError> {
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::<Vec<_>>()
})
.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::<Vec<_>>()
})
.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<RestImportParameter> {
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<Value> {
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<Value> {
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<ImportFinding> {
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<HttpMethod> {
match value {
"get" => Some(HttpMethod::Get),
"post" => Some(HttpMethod::Post),
"put" => Some(HttpMethod::Put),
"patch" => Some(HttpMethod::Patch),
"delete" => Some(HttpMethod::Delete),
_ => None,
}
}