use std::fs;
use httpgenerator_core::{
NormalizedHttpMethod, NormalizedInlineParameter, NormalizedInlineRequestBody,
NormalizedMediaType, NormalizedOpenApiDocument, NormalizedOperation, NormalizedParameter,
NormalizedParameterLocation, NormalizedRequestBody, NormalizedSchema, NormalizedSchemaProperty,
NormalizedSchemaType, NormalizedServer, NormalizedSpecificationVersion,
};
use serde_json::{Map, Value};
use crate::{
LoadedOpenApiDocument, OpenApiDocumentNormalizationError, OpenApiNormalizationError,
OpenApiSource,
loader::load_document_with_options,
};
pub fn load_and_normalize_document(
input: &str,
) -> Result<NormalizedOpenApiDocument, OpenApiDocumentNormalizationError> {
load_and_normalize_document_with_options(input, false)
}
pub fn load_and_normalize_document_with_options(
input: &str,
tolerate_invalid_openapi31: bool,
) -> Result<NormalizedOpenApiDocument, OpenApiDocumentNormalizationError> {
let document = load_document_with_options(input, tolerate_invalid_openapi31)
.map_err(OpenApiDocumentNormalizationError::Load)?;
normalize_loaded_document(&document).map_err(OpenApiDocumentNormalizationError::Normalize)
}
pub fn normalize_loaded_document(
document: &LoadedOpenApiDocument,
) -> Result<NormalizedOpenApiDocument, OpenApiNormalizationError> {
Ok(NormalizedOpenApiDocument {
specification_version: normalize_specification_version(document),
servers: normalize_servers(document)?,
operations: normalize_operations(document.raw().value())?,
})
}
fn normalize_specification_version(
document: &LoadedOpenApiDocument,
) -> NormalizedSpecificationVersion {
match document.specification_version() {
crate::OpenApiSpecificationVersion::Swagger2 => NormalizedSpecificationVersion::Swagger2,
crate::OpenApiSpecificationVersion::OpenApi30 => NormalizedSpecificationVersion::OpenApi30,
crate::OpenApiSpecificationVersion::OpenApi31 => NormalizedSpecificationVersion::OpenApi31,
}
}
fn normalize_servers(
document: &LoadedOpenApiDocument,
) -> Result<Vec<NormalizedServer>, OpenApiNormalizationError> {
let value = document.raw().value();
let Some(servers) = value.get("servers") else {
if value.get("swagger").is_some() {
return normalize_swagger2_servers(value, document.source());
}
return Ok(Vec::new());
};
let Some(servers) = servers.as_array() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: "servers".to_string(),
context: "expected an array".to_string(),
});
};
let mut normalized = Vec::with_capacity(servers.len());
for (index, server) in servers.iter().enumerate() {
let Some(server) = server.as_object() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: format!("servers[{index}]"),
context: "expected an object".to_string(),
});
};
if let Some(url) = server.get("url").and_then(Value::as_str) {
normalized.push(NormalizedServer {
url: url.to_string(),
});
}
}
Ok(normalized)
}
fn normalize_swagger2_servers(
value: &Value,
source: &OpenApiSource,
) -> Result<Vec<NormalizedServer>, OpenApiNormalizationError> {
let host = value
.get("host")
.and_then(Value::as_str)
.unwrap_or_default()
.trim();
let base_path = value
.get("basePath")
.and_then(Value::as_str)
.unwrap_or_default();
let schemes = value.get("schemes");
if host.is_empty() && base_path.is_empty() {
return Ok(local_swagger2_file_server(source)
.into_iter()
.map(|url| NormalizedServer { url })
.collect());
}
let schemes = match schemes {
Some(schemes) => {
let Some(schemes) = schemes.as_array() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: "schemes".to_string(),
context: "expected an array".to_string(),
});
};
schemes
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect::<Vec<_>>()
}
None => Vec::new(),
};
if host.is_empty() {
return Ok(vec![NormalizedServer {
url: base_path.to_string(),
}]);
}
if schemes.is_empty() {
return Ok(vec![NormalizedServer {
url: format!("https://{host}{base_path}"),
}]);
}
Ok(schemes
.into_iter()
.map(|scheme| NormalizedServer {
url: format!("{scheme}://{host}{base_path}"),
})
.collect())
}
fn local_swagger2_file_server(source: &OpenApiSource) -> Option<String> {
let OpenApiSource::Path(path) = source else {
return None;
};
let path = fs::canonicalize(path).unwrap_or_else(|_| path.clone());
let directory = path.parent()?;
let mut directory = directory.to_string_lossy().into_owned();
if let Some(stripped) = directory.strip_prefix(r"\\?\") {
directory = stripped.to_string();
}
directory = directory.replace('\\', "/");
Some(format!("file://{directory}"))
}
fn normalize_operations(
root: &Value,
) -> Result<Vec<NormalizedOperation>, OpenApiNormalizationError> {
let Some(paths) = root.get("paths") else {
return Ok(Vec::new());
};
let Some(paths) = paths.as_object() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: "paths".to_string(),
context: "expected an object".to_string(),
});
};
let mut operations = Vec::new();
for (path, path_item_value) in paths {
let Some(path_item) = path_item_value.as_object() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: path.clone(),
context: "expected a path item object".to_string(),
});
};
if let Some(reference) = path_item.get("$ref").and_then(Value::as_str) {
return Err(OpenApiNormalizationError::UnsupportedPathItemReference {
path: path.clone(),
reference: reference.to_string(),
});
}
let path_parameters = get_parameter_values(path_item, path, "parameters")?;
for method in supported_methods() {
let Some(operation_value) = path_item.get(method.as_str()) else {
continue;
};
let Some(operation) = operation_value.as_object() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: format!("paths.{path}.{}", method.as_str()),
context: "expected an operation object".to_string(),
});
};
operations.push(normalize_operation(
root,
path,
method,
&path_parameters,
operation,
)?);
}
}
Ok(operations)
}
fn normalize_operation(
root: &Value,
path: &str,
method: NormalizedHttpMethod,
path_parameters: &[&Value],
operation: &Map<String, Value>,
) -> Result<NormalizedOperation, OpenApiNormalizationError> {
Ok(NormalizedOperation {
path: path.to_string(),
method,
operation_id: operation
.get("operationId")
.and_then(Value::as_str)
.map(str::to_string),
summary: operation
.get("summary")
.and_then(Value::as_str)
.map(str::to_string),
description: operation
.get("description")
.and_then(Value::as_str)
.map(str::to_string),
tags: normalize_tags(operation)?,
parameters: normalize_parameters(root, path, method, path_parameters, operation)?,
request_body: normalize_request_body(root, path, method, operation)?,
})
}
fn normalize_tags(
operation: &Map<String, Value>,
) -> Result<Vec<String>, OpenApiNormalizationError> {
let Some(tags) = operation.get("tags") else {
return Ok(Vec::new());
};
let Some(tags) = tags.as_array() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: "operation.tags".to_string(),
context: "expected an array".to_string(),
});
};
Ok(tags
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect())
}
fn normalize_parameters(
root: &Value,
path: &str,
method: NormalizedHttpMethod,
path_parameters: &[&Value],
operation: &Map<String, Value>,
) -> Result<Vec<NormalizedParameter>, OpenApiNormalizationError> {
let operation_parameters = get_parameter_values(operation, path, "parameters")?;
let mut merged = Vec::new();
for parameter in path_parameters
.iter()
.copied()
.chain(operation_parameters.iter().copied())
{
let Some(normalized) = normalize_parameter(root, path, method, parameter)? else {
continue;
};
let parameter_key = normalized.inline_key();
if let Some(parameter_key) = parameter_key {
if let Some(index) = merged.iter().position(|existing: &NormalizedParameter| {
existing.inline_key() == Some(parameter_key)
}) {
merged[index] = normalized;
continue;
}
}
merged.push(normalized);
}
Ok(merged)
}
fn get_parameter_values<'a>(
object: &'a Map<String, Value>,
path: &str,
field_name: &str,
) -> Result<Vec<&'a Value>, OpenApiNormalizationError> {
let Some(parameters) = object.get(field_name) else {
return Ok(Vec::new());
};
let Some(parameters) = parameters.as_array() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: format!("{path}.{field_name}"),
context: "expected an array".to_string(),
});
};
Ok(parameters.iter().collect())
}
fn normalize_parameter(
root: &Value,
path: &str,
method: NormalizedHttpMethod,
value: &Value,
) -> Result<Option<NormalizedParameter>, OpenApiNormalizationError> {
if let Some(reference) = value.get("$ref").and_then(Value::as_str) {
return Err(OpenApiNormalizationError::UnsupportedParameterReference {
path: path.to_string(),
method,
reference: reference.to_string(),
});
}
let Some(parameter) = value.as_object() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: format!("{path}.{}", method.as_str()),
context: "expected a parameter object".to_string(),
});
};
let name = parameter
.get("name")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_default();
let location_name = parameter.get("in").and_then(Value::as_str).ok_or_else(|| {
OpenApiNormalizationError::InvalidStructure {
path: format!("{path}.{}.parameters", method.as_str()),
context: "parameter is missing a location".to_string(),
}
})?;
let Some(location) = normalize_parameter_location(location_name) else {
return Ok(None);
};
let synthetic_schema = synthesize_swagger2_parameter_schema(parameter);
Ok(Some(NormalizedParameter::Inline(
NormalizedInlineParameter {
name,
location,
description: parameter
.get("description")
.and_then(Value::as_str)
.map(str::to_string),
required: parameter
.get("required")
.and_then(Value::as_bool)
.unwrap_or(false),
schema: parameter
.get("schema")
.or(synthetic_schema.as_ref())
.map(|schema| normalize_schema(root, schema)),
},
)))
}
fn synthesize_swagger2_parameter_schema(parameter: &Map<String, Value>) -> Option<Value> {
if parameter.get("schema").is_some() {
return None;
}
let mut schema = Map::new();
for field_name in ["type", "items", "allOf", "oneOf", "anyOf", "properties"] {
if let Some(value) = parameter.get(field_name) {
schema.insert(field_name.to_string(), value.clone());
}
}
if schema.is_empty() {
None
} else {
Some(Value::Object(schema))
}
}
fn normalize_request_body(
root: &Value,
path: &str,
method: NormalizedHttpMethod,
operation: &Map<String, Value>,
) -> Result<Option<NormalizedRequestBody>, OpenApiNormalizationError> {
if let Some(request_body) = operation.get("requestBody") {
return normalize_openapi3_request_body(root, path, method, request_body);
}
normalize_swagger2_request_body(root, path, method, operation)
}
fn normalize_openapi3_request_body(
root: &Value,
path: &str,
method: NormalizedHttpMethod,
request_body: &Value,
) -> Result<Option<NormalizedRequestBody>, OpenApiNormalizationError> {
if let Some(reference) = request_body.get("$ref").and_then(Value::as_str) {
return Err(OpenApiNormalizationError::UnsupportedRequestBodyReference {
path: path.to_string(),
method,
reference: reference.to_string(),
});
}
let Some(request_body) = request_body.as_object() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: format!("{path}.{}.requestBody", method.as_str()),
context: "expected a requestBody object".to_string(),
});
};
Ok(Some(NormalizedRequestBody::Inline(
NormalizedInlineRequestBody {
description: request_body
.get("description")
.and_then(Value::as_str)
.map(str::to_string),
required: request_body
.get("required")
.and_then(Value::as_bool)
.unwrap_or(false),
content: normalize_request_body_content(root, path, method, request_body)?,
},
)))
}
fn normalize_swagger2_request_body(
root: &Value,
path: &str,
method: NormalizedHttpMethod,
operation: &Map<String, Value>,
) -> Result<Option<NormalizedRequestBody>, OpenApiNormalizationError> {
let Some(parameters) = operation.get("parameters").and_then(Value::as_array) else {
return Ok(None);
};
let Some(body_parameter) = parameters.iter().find(|parameter| {
parameter
.get("in")
.and_then(Value::as_str)
.is_some_and(|location| location == "body")
}) else {
return Ok(None);
};
if let Some(reference) = body_parameter.get("$ref").and_then(Value::as_str) {
return Err(OpenApiNormalizationError::UnsupportedRequestBodyReference {
path: path.to_string(),
method,
reference: reference.to_string(),
});
}
let Some(body_parameter) = body_parameter.as_object() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: format!("{path}.{}.parameters.body", method.as_str()),
context: "expected a body parameter object".to_string(),
});
};
Ok(Some(NormalizedRequestBody::Inline(
NormalizedInlineRequestBody {
description: body_parameter
.get("description")
.and_then(Value::as_str)
.map(str::to_string),
required: body_parameter
.get("required")
.and_then(Value::as_bool)
.unwrap_or(false),
content: normalize_swagger2_request_body_content(root, body_parameter, operation),
},
)))
}
fn normalize_request_body_content(
root: &Value,
path: &str,
method: NormalizedHttpMethod,
request_body: &Map<String, Value>,
) -> Result<Vec<NormalizedMediaType>, OpenApiNormalizationError> {
match request_body.get("content") {
Some(content) => {
let Some(content) = content.as_object() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: format!("{path}.{}.requestBody.content", method.as_str()),
context: "expected a content object".to_string(),
});
};
content
.iter()
.map(|(content_type, media_type)| {
let Some(media_type) = media_type.as_object() else {
return Err(OpenApiNormalizationError::InvalidStructure {
path: format!(
"{path}.{}.requestBody.content.{content_type}",
method.as_str()
),
context: "expected a media type object".to_string(),
});
};
Ok(NormalizedMediaType {
content_type: content_type.clone(),
schema: media_type
.get("schema")
.map(|schema| normalize_schema(root, schema)),
})
})
.collect::<Result<Vec<_>, _>>()
}
None => Ok(Vec::new()),
}
}
fn normalize_swagger2_request_body_content(
root: &Value,
body_parameter: &Map<String, Value>,
operation: &Map<String, Value>,
) -> Vec<NormalizedMediaType> {
let content_types = operation
.get("consumes")
.or_else(|| root.get("consumes"))
.and_then(Value::as_array)
.map(|content_types| {
content_types
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect::<Vec<_>>()
})
.filter(|content_types| !content_types.is_empty())
.unwrap_or_else(|| vec!["application/json".to_string()]);
content_types
.into_iter()
.map(|content_type| NormalizedMediaType {
content_type,
schema: body_parameter
.get("schema")
.map(|schema| normalize_schema(root, schema)),
})
.collect()
}
fn normalize_schema(root: &Value, value: &Value) -> NormalizedSchema {
let mut resolution_stack = Vec::new();
normalize_schema_with_resolution(root, value, &mut resolution_stack)
}
fn normalize_schema_with_resolution(
root: &Value,
value: &Value,
resolution_stack: &mut Vec<String>,
) -> NormalizedSchema {
match value {
Value::Object(schema) => {
let reference = schema
.get("$ref")
.and_then(Value::as_str)
.map(str::to_string);
let mut normalized = reference
.as_deref()
.and_then(|reference| resolve_internal_reference(root, reference, resolution_stack))
.unwrap_or_default();
let overlay = NormalizedSchema {
reference,
types: normalize_schema_types(schema.get("type")),
properties: schema
.get("properties")
.and_then(Value::as_object)
.map(|properties| {
properties
.iter()
.map(|(name, property)| NormalizedSchemaProperty {
name: name.clone(),
schema: normalize_schema_with_resolution(
root,
property,
resolution_stack,
),
})
.collect()
})
.unwrap_or_default(),
items: schema.get("items").map(|items| {
Box::new(normalize_schema_with_resolution(
root,
items,
resolution_stack,
))
}),
all_of: normalize_schema_array(root, schema.get("allOf"), resolution_stack),
one_of: normalize_schema_array(root, schema.get("oneOf"), resolution_stack),
any_of: normalize_schema_array(root, schema.get("anyOf"), resolution_stack),
};
merge_schema(&mut normalized, overlay);
normalized
}
Value::Bool(value) => NormalizedSchema {
types: vec![NormalizedSchemaType::Other(format!(
"boolean-schema:{value}"
))],
..NormalizedSchema::default()
},
_ => NormalizedSchema::default(),
}
}
fn normalize_schema_array(
root: &Value,
value: Option<&Value>,
resolution_stack: &mut Vec<String>,
) -> Vec<NormalizedSchema> {
value
.and_then(Value::as_array)
.map(|schemas| {
schemas
.iter()
.map(|schema| normalize_schema_with_resolution(root, schema, resolution_stack))
.collect()
})
.unwrap_or_default()
}
fn resolve_internal_reference(
root: &Value,
reference: &str,
resolution_stack: &mut Vec<String>,
) -> Option<NormalizedSchema> {
if !reference.starts_with("#/") || resolution_stack.iter().any(|value| value == reference) {
return None;
}
let target = root.pointer(&reference[1..])?;
resolution_stack.push(reference.to_string());
let resolved = normalize_schema_with_resolution(root, target, resolution_stack);
resolution_stack.pop();
Some(resolved)
}
fn merge_schema(base: &mut NormalizedSchema, overlay: NormalizedSchema) {
if overlay.reference.is_some() {
base.reference = overlay.reference;
}
if !overlay.types.is_empty() {
base.types = overlay.types;
}
if !overlay.properties.is_empty() {
base.properties = overlay.properties;
}
if overlay.items.is_some() {
base.items = overlay.items;
}
if !overlay.all_of.is_empty() {
base.all_of = overlay.all_of;
}
if !overlay.one_of.is_empty() {
base.one_of = overlay.one_of;
}
if !overlay.any_of.is_empty() {
base.any_of = overlay.any_of;
}
}
fn normalize_schema_types(value: Option<&Value>) -> Vec<NormalizedSchemaType> {
match value {
Some(Value::String(schema_type)) => vec![normalize_schema_type(schema_type)],
Some(Value::Array(types)) => types
.iter()
.filter_map(Value::as_str)
.map(normalize_schema_type)
.collect(),
_ => Vec::new(),
}
}
fn normalize_schema_type(value: &str) -> NormalizedSchemaType {
match value {
"string" => NormalizedSchemaType::String,
"integer" => NormalizedSchemaType::Integer,
"number" => NormalizedSchemaType::Number,
"boolean" => NormalizedSchemaType::Boolean,
"object" => NormalizedSchemaType::Object,
"array" => NormalizedSchemaType::Array,
"null" => NormalizedSchemaType::Null,
other => NormalizedSchemaType::Other(other.to_string()),
}
}
fn normalize_parameter_location(value: &str) -> Option<NormalizedParameterLocation> {
match value {
"path" => Some(NormalizedParameterLocation::Path),
"query" => Some(NormalizedParameterLocation::Query),
"header" => Some(NormalizedParameterLocation::Header),
"cookie" => Some(NormalizedParameterLocation::Cookie),
_ => None,
}
}
fn supported_methods() -> [NormalizedHttpMethod; 8] {
[
NormalizedHttpMethod::Get,
NormalizedHttpMethod::Put,
NormalizedHttpMethod::Post,
NormalizedHttpMethod::Delete,
NormalizedHttpMethod::Options,
NormalizedHttpMethod::Head,
NormalizedHttpMethod::Patch,
NormalizedHttpMethod::Trace,
]
}
trait InlineParameterKey {
fn inline_key(&self) -> Option<(&str, NormalizedParameterLocation)>;
}
impl InlineParameterKey for NormalizedParameter {
fn inline_key(&self) -> Option<(&str, NormalizedParameterLocation)> {
match self {
NormalizedParameter::Inline(parameter) => Some((¶meter.name, parameter.location)),
NormalizedParameter::Reference { .. } => None,
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use httpgenerator_core::{
NormalizedHttpMethod, NormalizedParameter, NormalizedParameterLocation,
NormalizedRequestBody, NormalizedServer, NormalizedSpecificationVersion,
};
use crate::{OpenApiSource, decode_raw_document, load_document_from_raw};
use super::{
load_and_normalize_document, load_and_normalize_document_with_options,
normalize_loaded_document,
};
#[test]
fn normalizes_petstore_v30_fixture_into_generator_facing_operations() {
let raw = decode_raw_document(
OpenApiSource::Path(PathBuf::from("test/OpenAPI/v3.0/petstore.json")),
include_str!("../../../../test/OpenAPI/v3.0/petstore.json"),
)
.unwrap();
let loaded = load_document_from_raw(raw).unwrap();
let normalized = normalize_loaded_document(&loaded).unwrap();
assert_eq!(
normalized.specification_version,
NormalizedSpecificationVersion::OpenApi30
);
assert_eq!(normalized.servers[0].url, "/api/v3");
assert_eq!(normalized.operations.len(), 19);
let add_pet = normalized
.operations
.iter()
.find(|operation| {
operation.path == "/pet" && operation.method == NormalizedHttpMethod::Post
})
.unwrap();
assert_eq!(add_pet.tags.first().map(String::as_str), Some("pet"));
match add_pet.request_body.as_ref().unwrap() {
NormalizedRequestBody::Inline(request_body) => {
let application_json = request_body
.content
.iter()
.find(|content| content.content_type == "application/json")
.unwrap();
let schema = application_json.schema.as_ref().unwrap();
assert_eq!(
schema.reference.as_deref(),
Some("#/components/schemas/Pet")
);
assert_eq!(
schema
.properties
.iter()
.take(3)
.map(|property| property.name.as_str())
.collect::<Vec<_>>(),
vec!["id", "name", "category"]
);
let category = schema
.properties
.iter()
.find(|property| property.name == "category")
.unwrap();
assert!(
category
.schema
.types
.contains(&httpgenerator_core::NormalizedSchemaType::Object)
);
}
NormalizedRequestBody::Reference { .. } => {
panic!("expected addPet to use an inline request body")
}
}
let find_by_status = normalized
.operations
.iter()
.find(|operation| {
operation.path == "/pet/findByStatus"
&& operation.method == NormalizedHttpMethod::Get
})
.unwrap();
assert!(find_by_status.parameters.iter().any(|parameter| {
matches!(
parameter,
NormalizedParameter::Inline(parameter)
if parameter.name == "status"
&& parameter.location == NormalizedParameterLocation::Query
)
}));
}
#[test]
fn normalizes_petstore_v20_fixture_into_generator_facing_operations() {
let raw = decode_raw_document(
OpenApiSource::Path(PathBuf::from("test/OpenAPI/v2.0/petstore.json")),
include_str!("../../../../test/OpenAPI/v2.0/petstore.json"),
)
.unwrap();
let loaded = load_document_from_raw(raw).unwrap();
let normalized = normalize_loaded_document(&loaded).unwrap();
assert_eq!(
normalized.specification_version,
NormalizedSpecificationVersion::Swagger2
);
assert_eq!(normalized.servers[0].url, "https://petstore.swagger.io/v2");
assert_eq!(normalized.operations.len(), 20);
assert!(normalized.operations.iter().any(|operation| {
operation.path == "/user/createWithArray"
&& operation.method == NormalizedHttpMethod::Post
}));
let add_pet = normalized
.operations
.iter()
.find(|operation| {
operation.path == "/pet" && operation.method == NormalizedHttpMethod::Post
})
.unwrap();
match add_pet.request_body.as_ref().unwrap() {
NormalizedRequestBody::Inline(request_body) => {
assert_eq!(
request_body
.content
.iter()
.map(|content| content.content_type.as_str())
.collect::<Vec<_>>(),
vec!["application/json", "application/xml"]
);
let schema = request_body.content[0].schema.as_ref().unwrap();
assert_eq!(schema.reference.as_deref(), Some("#/definitions/Pet"));
assert_eq!(
schema
.properties
.iter()
.take(3)
.map(|property| property.name.as_str())
.collect::<Vec<_>>(),
vec!["id", "category", "name"]
);
}
NormalizedRequestBody::Reference { .. } => {
panic!("expected addPet to use an inline Swagger 2 request body")
}
}
let find_by_status = normalized
.operations
.iter()
.find(|operation| {
operation.path == "/pet/findByStatus"
&& operation.method == NormalizedHttpMethod::Get
})
.unwrap();
assert!(find_by_status.parameters.iter().any(|parameter| {
matches!(
parameter,
NormalizedParameter::Inline(parameter)
if parameter.name == "status"
&& parameter.location == NormalizedParameterLocation::Query
&& parameter
.schema
.as_ref()
.is_some_and(|schema| schema.types.contains(&httpgenerator_core::NormalizedSchemaType::Array))
)
}));
let upload_image = normalized
.operations
.iter()
.find(|operation| {
operation.path == "/pet/{petId}/uploadImage"
&& operation.method == NormalizedHttpMethod::Post
})
.unwrap();
assert_eq!(upload_image.parameters.len(), 1);
assert!(matches!(
&upload_image.parameters[0],
NormalizedParameter::Inline(parameter)
if parameter.name == "petId"
&& parameter.location == NormalizedParameterLocation::Path
));
let update_pet_with_form = normalized
.operations
.iter()
.find(|operation| {
operation.path == "/pet/{petId}" && operation.method == NormalizedHttpMethod::Post
})
.unwrap();
assert_eq!(update_pet_with_form.parameters.len(), 1);
assert!(update_pet_with_form.request_body.is_none());
}
#[test]
fn swagger2_local_documents_without_host_or_base_path_use_parent_directory_server() {
let input = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("..")
.join("test")
.join("OpenAPI")
.join("v2.0")
.join("api-with-examples.json");
let normalized = load_and_normalize_document(input.to_str().unwrap()).unwrap();
let mut expected_directory = std::fs::canonicalize(&input)
.unwrap()
.parent()
.unwrap()
.to_string_lossy()
.into_owned();
if let Some(stripped) = expected_directory.strip_prefix(r"\\?\") {
expected_directory = stripped.to_string();
}
expected_directory = expected_directory.replace('\\', "/");
assert_eq!(
normalized.servers,
vec![NormalizedServer {
url: format!("file://{expected_directory}"),
}]
);
}
#[test]
fn openapi30_local_documents_without_servers_do_not_use_parent_directory_server() {
let input = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("..")
.join("test")
.join("OpenAPI")
.join("v3.0")
.join("api-with-examples.json");
let normalized = load_and_normalize_document(input.to_str().unwrap()).unwrap();
assert!(normalized.servers.is_empty());
}
#[test]
fn webhook_only_v31_documents_normalize_without_operations() {
let raw = decode_raw_document(
OpenApiSource::Path(PathBuf::from("test/OpenAPI/v3.1/webhook-example.json")),
include_str!("../../../../test/OpenAPI/v3.1/webhook-example.json"),
)
.unwrap();
let loaded = load_document_from_raw(raw).unwrap();
let normalized = normalize_loaded_document(&loaded).unwrap();
assert_eq!(
normalized.specification_version,
NormalizedSpecificationVersion::OpenApi31
);
assert!(normalized.servers.is_empty());
assert!(normalized.operations.is_empty());
}
#[test]
fn invalid_v31_documents_normalize_when_tolerated() {
let normalized = load_and_normalize_document_with_options(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("..")
.join("test")
.join("OpenAPI")
.join("v3.1")
.join("non-oauth-scopes.json")
.to_str()
.unwrap(),
true,
)
.unwrap();
assert_eq!(
normalized.specification_version,
NormalizedSpecificationVersion::OpenApi31
);
assert_eq!(normalized.operations.len(), 1);
assert_eq!(normalized.operations[0].path, "/users");
assert_eq!(normalized.operations[0].method, NormalizedHttpMethod::Get);
}
#[test]
fn operation_level_parameters_override_path_level_parameters() {
let raw = decode_raw_document(
OpenApiSource::Path(PathBuf::from("inline.json")),
r#"{
"openapi": "3.0.2",
"info": { "title": "Example", "version": "1.0.0" },
"paths": {
"/pets": {
"parameters": [
{
"name": "status",
"in": "query",
"description": "path-level",
"schema": { "type": "string" }
}
],
"get": {
"parameters": [
{
"name": "status",
"in": "query",
"description": "operation-level",
"schema": { "type": "string" }
}
],
"responses": {
"200": {
"description": "ok"
}
}
}
}
}
}"#,
)
.unwrap();
let loaded = load_document_from_raw(raw).unwrap();
let normalized = normalize_loaded_document(&loaded).unwrap();
assert_eq!(normalized.operations.len(), 1);
assert_eq!(normalized.operations[0].parameters.len(), 1);
match &normalized.operations[0].parameters[0] {
NormalizedParameter::Inline(parameter) => {
assert_eq!(parameter.description.as_deref(), Some("operation-level"));
}
NormalizedParameter::Reference { .. } => panic!("expected an inline parameter"),
}
}
#[test]
fn top_level_request_body_refs_fail_explicitly_during_normalization() {
let raw = decode_raw_document(
OpenApiSource::Path(PathBuf::from("inline.json")),
r##"{
"openapi": "3.0.2",
"info": { "title": "Example", "version": "1.0.0" },
"paths": {
"/pets": {
"post": {
"requestBody": {
"$ref": "#/components/requestBodies/PetBody"
},
"responses": {
"200": {
"description": "ok"
}
}
}
}
}
}"##,
)
.unwrap();
let loaded = load_document_from_raw(raw).unwrap();
let error = normalize_loaded_document(&loaded).unwrap_err();
assert_eq!(
error,
crate::OpenApiNormalizationError::UnsupportedRequestBodyReference {
path: "/pets".to_string(),
method: NormalizedHttpMethod::Post,
reference: "#/components/requestBodies/PetBody".to_string(),
}
);
}
#[test]
fn convenience_loader_normalizes_local_documents() {
let normalized = load_and_normalize_document(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("..")
.join("test")
.join("OpenAPI")
.join("v3.0")
.join("petstore.json")
.to_str()
.unwrap(),
)
.unwrap();
assert_eq!(
normalized.specification_version,
NormalizedSpecificationVersion::OpenApi30
);
}
}