Add OpenAPI import backend
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user