229 lines
8.2 KiB
Rust
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,
|
|
}
|
|
}
|