feat(import): add deterministic OpenAPI normalized IR
This commit is contained in:
@@ -0,0 +1,767 @@
|
||||
mod normalization_details {
|
||||
use crank_import::rest::{
|
||||
ImportFindingSeverity, ImportParseError, NormalizationConfig, normalize_verified_document,
|
||||
preview_document, preview_document_legacy_v1,
|
||||
};
|
||||
|
||||
fn normalize_document(
|
||||
document: &str,
|
||||
config: &NormalizationConfig,
|
||||
) -> Result<crank_import::rest::NormalizedIr, ImportParseError> {
|
||||
normalize_verified_document(
|
||||
document,
|
||||
crank_import::rest::SourceDigest::parse("b".repeat(64)).unwrap(),
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_identity_does_not_depend_on_operation_id() {
|
||||
let document = r#"
|
||||
openapi: 3.0.3
|
||||
info: { title: Identity }
|
||||
paths:
|
||||
/same:
|
||||
get:
|
||||
operationId: duplicate
|
||||
responses: { '200': { description: ok } }
|
||||
post:
|
||||
operationId: duplicate
|
||||
responses: { '201': { description: ok } }
|
||||
"#;
|
||||
let ir = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
||||
assert_ne!(ir.operations[0].stable_id, ir.operations[1].stable_id);
|
||||
assert_eq!(ir.operations[0].key, "GET /same");
|
||||
assert_eq!(ir.operations[1].key, "POST /same");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blank_and_duplicate_operation_ids_have_exact_findings_and_path_servers_win() {
|
||||
let document = r#"
|
||||
openapi: 3.0.3
|
||||
info: { title: IDs }
|
||||
servers: [{ url: https://document.test }]
|
||||
paths:
|
||||
/one:
|
||||
servers: [{ url: https://path.test }]
|
||||
get:
|
||||
operationId: duplicate
|
||||
responses: { '200': { description: ok } }
|
||||
/two:
|
||||
get:
|
||||
operationId: duplicate
|
||||
responses: { '200': { description: ok } }
|
||||
/blank:
|
||||
get:
|
||||
operationId: ' '
|
||||
responses: { '200': { description: ok } }
|
||||
"#;
|
||||
let ir = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
||||
let duplicate = ir
|
||||
.operations
|
||||
.iter()
|
||||
.filter(|operation| {
|
||||
operation
|
||||
.findings
|
||||
.iter()
|
||||
.any(|finding| finding.code == "duplicate_operation_id")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(duplicate.len(), 2);
|
||||
assert!(duplicate.iter().all(|operation| {
|
||||
operation
|
||||
.findings
|
||||
.iter()
|
||||
.any(|finding| finding.location.pointer.ends_with("/operationId"))
|
||||
}));
|
||||
let blank = ir
|
||||
.operations
|
||||
.iter()
|
||||
.find(|operation| operation.path == "/blank")
|
||||
.unwrap();
|
||||
assert!(
|
||||
blank
|
||||
.findings
|
||||
.iter()
|
||||
.any(|finding| finding.code == "missing_operation_id")
|
||||
);
|
||||
let preview = preview_document(document).unwrap();
|
||||
let blank_preview = preview
|
||||
.groups
|
||||
.iter()
|
||||
.flat_map(|group| &group.operations)
|
||||
.find(|operation| operation.path == "/blank")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
blank_preview
|
||||
.findings
|
||||
.iter()
|
||||
.filter(|finding| finding.code == "missing_operation_id")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
preview
|
||||
.groups
|
||||
.iter()
|
||||
.flat_map(|group| &group.operations)
|
||||
.find(|operation| operation.path == "/one")
|
||||
.unwrap()
|
||||
.server_urls,
|
||||
vec!["https://path.test"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parameters_keep_exact_path_and_operation_source_pointers() {
|
||||
let openapi = r#"
|
||||
openapi: 3.0.3
|
||||
info: { title: Parameter locations }
|
||||
paths:
|
||||
/items/{id}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
operationId: getItem
|
||||
parameters:
|
||||
- { name: filter, in: query, schema: { type: string } }
|
||||
responses: { '200': { description: ok } }
|
||||
"#;
|
||||
let ir = normalize_document(openapi, &NormalizationConfig::default()).unwrap();
|
||||
let parameters = &ir.operations[0].parameters;
|
||||
assert_eq!(
|
||||
parameters[0].source_location.pointer,
|
||||
"/paths/~1items~1{id}/parameters/0"
|
||||
);
|
||||
assert_eq!(
|
||||
parameters[1].source_location.pointer,
|
||||
"/paths/~1items~1{id}/get/parameters/0"
|
||||
);
|
||||
assert_ne!(parameters[0].construct_id, parameters[1].construct_id);
|
||||
|
||||
let swagger = r#"
|
||||
swagger: '2.0'
|
||||
info: { title: Swagger parameter locations }
|
||||
paths:
|
||||
/items/{id}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, type: string }
|
||||
post:
|
||||
operationId: updateItem
|
||||
parameters:
|
||||
- { name: body, in: body, schema: { type: object } }
|
||||
responses: { '200': { description: ok } }
|
||||
"#;
|
||||
let ir = normalize_document(swagger, &NormalizationConfig::default()).unwrap();
|
||||
let parameter = &ir.operations[0].parameters[0];
|
||||
assert_eq!(
|
||||
parameter.source_location.pointer,
|
||||
"/paths/~1items~1{id}/parameters/0"
|
||||
);
|
||||
assert!(ir.operations[0].request_body_schema.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openapi_parameter_omissions_are_errors_at_the_dropped_item_pointer() {
|
||||
let document = r#"
|
||||
openapi: 3.1.0
|
||||
info: { title: Parameter findings }
|
||||
servers: [{ url: https://example.test }]
|
||||
paths:
|
||||
/items/{id}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
- { name: path_cookie, in: cookie }
|
||||
- not-an-object
|
||||
- { $ref: '#/components/parameters/Id' }
|
||||
get:
|
||||
operationId: getItem
|
||||
parameters:
|
||||
- { in: query }
|
||||
- { name: missing_location }
|
||||
- { name: form_value, in: formData }
|
||||
- { name: operation_cookie, in: cookie }
|
||||
- { name: query, in: query, schema: { type: string } }
|
||||
responses: { '200': { description: ok } }
|
||||
components:
|
||||
parameters:
|
||||
Id: { name: id, in: path, required: true, schema: { type: string } }
|
||||
"#;
|
||||
let ir = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
||||
assert_eq!(
|
||||
ir.operations[0]
|
||||
.parameters
|
||||
.iter()
|
||||
.map(|parameter| parameter.name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["id", "query"]
|
||||
);
|
||||
let errors = ir
|
||||
.findings
|
||||
.iter()
|
||||
.filter(|finding| {
|
||||
[
|
||||
"invalid_parameter",
|
||||
"missing_parameter_name",
|
||||
"missing_parameter_location",
|
||||
"unsupported_parameter_location",
|
||||
"unsupported_cookie_parameter",
|
||||
"unresolved_parameter_reference",
|
||||
]
|
||||
.contains(&finding.code.as_str())
|
||||
})
|
||||
.map(|finding| (finding.code.as_str(), finding.location.pointer.as_str()))
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
errors,
|
||||
std::collections::BTreeSet::from([
|
||||
(
|
||||
"unsupported_cookie_parameter",
|
||||
"/paths/~1items~1{id}/parameters/1",
|
||||
),
|
||||
("invalid_parameter", "/paths/~1items~1{id}/parameters/2",),
|
||||
(
|
||||
"unresolved_parameter_reference",
|
||||
"/paths/~1items~1{id}/parameters/3",
|
||||
),
|
||||
(
|
||||
"missing_parameter_name",
|
||||
"/paths/~1items~1{id}/get/parameters/0",
|
||||
),
|
||||
(
|
||||
"missing_parameter_location",
|
||||
"/paths/~1items~1{id}/get/parameters/1",
|
||||
),
|
||||
(
|
||||
"unsupported_parameter_location",
|
||||
"/paths/~1items~1{id}/get/parameters/2",
|
||||
),
|
||||
(
|
||||
"unsupported_cookie_parameter",
|
||||
"/paths/~1items~1{id}/get/parameters/3",
|
||||
),
|
||||
])
|
||||
);
|
||||
assert!(
|
||||
ir.findings
|
||||
.iter()
|
||||
.filter(|finding| {
|
||||
[
|
||||
"invalid_parameter",
|
||||
"missing_parameter_name",
|
||||
"missing_parameter_location",
|
||||
"unsupported_parameter_location",
|
||||
"unsupported_cookie_parameter",
|
||||
"unresolved_parameter_reference",
|
||||
]
|
||||
.contains(&finding.code.as_str())
|
||||
})
|
||||
.all(|finding| finding.severity == ImportFindingSeverity::Error)
|
||||
);
|
||||
|
||||
let legacy = preview_document_legacy_v1(document).unwrap();
|
||||
assert!(!legacy.findings.iter().any(|finding| {
|
||||
[
|
||||
"invalid_parameter",
|
||||
"missing_parameter_name",
|
||||
"missing_parameter_location",
|
||||
"unsupported_parameter_location",
|
||||
"unresolved_parameter_reference",
|
||||
]
|
||||
.contains(&finding.code.as_str())
|
||||
}));
|
||||
assert!(
|
||||
!legacy.groups[0].operations[0]
|
||||
.findings
|
||||
.iter()
|
||||
.any(|finding| finding.code == "unsupported_cookie_parameter")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swagger_parameter_omissions_are_errors_at_the_dropped_item_pointer() {
|
||||
let document = r#"
|
||||
swagger: '2.0'
|
||||
info: { title: Swagger parameter findings }
|
||||
host: example.test
|
||||
paths:
|
||||
/items/{id}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, type: string }
|
||||
- { name: path_cookie, in: cookie }
|
||||
- not-an-object
|
||||
- { $ref: '#/parameters/Id' }
|
||||
get:
|
||||
operationId: getItem
|
||||
parameters:
|
||||
- { in: query }
|
||||
- { name: missing_location }
|
||||
- { name: form_value, in: formData }
|
||||
- { name: operation_cookie, in: cookie }
|
||||
- { name: query, in: query, type: string }
|
||||
responses: { '200': { description: ok } }
|
||||
parameters:
|
||||
Id: { name: id, in: path, required: true, type: string }
|
||||
"#;
|
||||
let ir = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
||||
assert_eq!(
|
||||
ir.operations[0]
|
||||
.parameters
|
||||
.iter()
|
||||
.map(|parameter| parameter.name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["id", "query"]
|
||||
);
|
||||
let errors = ir
|
||||
.findings
|
||||
.iter()
|
||||
.filter(|finding| {
|
||||
[
|
||||
"invalid_parameter",
|
||||
"missing_parameter_name",
|
||||
"missing_parameter_location",
|
||||
"unsupported_parameter_location",
|
||||
"unsupported_cookie_parameter",
|
||||
"unresolved_parameter_reference",
|
||||
]
|
||||
.contains(&finding.code.as_str())
|
||||
})
|
||||
.map(|finding| (finding.code.as_str(), finding.location.pointer.as_str()))
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
errors,
|
||||
std::collections::BTreeSet::from([
|
||||
(
|
||||
"unsupported_cookie_parameter",
|
||||
"/paths/~1items~1{id}/parameters/1",
|
||||
),
|
||||
("invalid_parameter", "/paths/~1items~1{id}/parameters/2",),
|
||||
(
|
||||
"unresolved_parameter_reference",
|
||||
"/paths/~1items~1{id}/parameters/3",
|
||||
),
|
||||
(
|
||||
"missing_parameter_name",
|
||||
"/paths/~1items~1{id}/get/parameters/0",
|
||||
),
|
||||
(
|
||||
"missing_parameter_location",
|
||||
"/paths/~1items~1{id}/get/parameters/1",
|
||||
),
|
||||
(
|
||||
"unsupported_parameter_location",
|
||||
"/paths/~1items~1{id}/get/parameters/2",
|
||||
),
|
||||
(
|
||||
"unsupported_cookie_parameter",
|
||||
"/paths/~1items~1{id}/get/parameters/3",
|
||||
),
|
||||
])
|
||||
);
|
||||
assert!(
|
||||
ir.findings
|
||||
.iter()
|
||||
.filter(|finding| {
|
||||
[
|
||||
"invalid_parameter",
|
||||
"missing_parameter_name",
|
||||
"missing_parameter_location",
|
||||
"unsupported_parameter_location",
|
||||
"unsupported_cookie_parameter",
|
||||
"unresolved_parameter_reference",
|
||||
]
|
||||
.contains(&finding.code.as_str())
|
||||
})
|
||||
.all(|finding| finding.severity == ImportFindingSeverity::Error)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openapi_invalid_servers_and_tags_keep_valid_siblings_with_exact_findings() {
|
||||
let document = r#"
|
||||
openapi: 3.1.0
|
||||
info: { title: Server and tag findings }
|
||||
servers:
|
||||
- { url: https://document.test/ }
|
||||
- not-an-object
|
||||
- {}
|
||||
- { url: 42 }
|
||||
paths:
|
||||
/items:
|
||||
servers:
|
||||
- { url: https://path.test/ }
|
||||
- false
|
||||
- {}
|
||||
get:
|
||||
operationId: getItems
|
||||
servers:
|
||||
- { url: https://operation.test/ }
|
||||
- { url: false }
|
||||
tags: [items, 42, {}, null]
|
||||
responses: { '200': { description: ok } }
|
||||
"#;
|
||||
let ir = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
||||
assert_eq!(ir.source.servers, vec!["https://document.test"]);
|
||||
assert_eq!(ir.operations[0].servers, vec!["https://operation.test"]);
|
||||
assert_eq!(ir.operations[0].tags, vec!["items"]);
|
||||
|
||||
let server_errors = ir
|
||||
.findings
|
||||
.iter()
|
||||
.filter(|finding| finding.code == "invalid_server")
|
||||
.map(|finding| finding.location.pointer.as_str())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
server_errors,
|
||||
std::collections::BTreeSet::from([
|
||||
"/servers/1",
|
||||
"/servers/2",
|
||||
"/servers/3",
|
||||
"/paths/~1items/servers/1",
|
||||
"/paths/~1items/servers/2",
|
||||
"/paths/~1items/get/servers/1",
|
||||
])
|
||||
);
|
||||
assert!(
|
||||
ir.findings
|
||||
.iter()
|
||||
.filter(|finding| finding.code == "invalid_server")
|
||||
.all(|finding| finding.severity == ImportFindingSeverity::Error)
|
||||
);
|
||||
let tag_warnings = ir
|
||||
.findings
|
||||
.iter()
|
||||
.filter(|finding| finding.code == "invalid_tag")
|
||||
.map(|finding| finding.location.pointer.as_str())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
tag_warnings,
|
||||
std::collections::BTreeSet::from([
|
||||
"/paths/~1items/get/tags/1",
|
||||
"/paths/~1items/get/tags/2",
|
||||
"/paths/~1items/get/tags/3",
|
||||
])
|
||||
);
|
||||
assert!(
|
||||
ir.findings
|
||||
.iter()
|
||||
.filter(|finding| finding.code == "invalid_tag")
|
||||
.all(|finding| finding.severity == ImportFindingSeverity::Warning)
|
||||
);
|
||||
|
||||
let legacy = preview_document_legacy_v1(document).unwrap();
|
||||
assert_eq!(legacy.source.servers, vec!["https://document.test"]);
|
||||
assert_eq!(
|
||||
legacy.groups[0].operations[0].server_urls,
|
||||
vec!["https://operation.test"]
|
||||
);
|
||||
assert_eq!(legacy.groups[0].operations[0].category, "items");
|
||||
assert!(
|
||||
!legacy.findings.iter().any(|finding| {
|
||||
["invalid_server", "invalid_tag"].contains(&finding.code.as_str())
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swagger_invalid_tags_keep_valid_siblings_with_exact_warning_pointers() {
|
||||
let document = r#"
|
||||
swagger: '2.0'
|
||||
info: { title: Swagger tag findings }
|
||||
host: example.test
|
||||
paths:
|
||||
/items:
|
||||
get:
|
||||
operationId: getItems
|
||||
tags: [items, 42, {}, null]
|
||||
responses: { '200': { description: ok } }
|
||||
"#;
|
||||
let ir = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
||||
assert_eq!(ir.operations[0].tags, vec!["items"]);
|
||||
let warnings = ir
|
||||
.findings
|
||||
.iter()
|
||||
.filter(|finding| finding.code == "invalid_tag")
|
||||
.map(|finding| finding.location.pointer.as_str())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
warnings,
|
||||
std::collections::BTreeSet::from([
|
||||
"/paths/~1items/get/tags/1",
|
||||
"/paths/~1items/get/tags/2",
|
||||
"/paths/~1items/get/tags/3",
|
||||
])
|
||||
);
|
||||
assert!(
|
||||
ir.findings
|
||||
.iter()
|
||||
.filter(|finding| finding.code == "invalid_tag")
|
||||
.all(|finding| finding.severity == ImportFindingSeverity::Warning)
|
||||
);
|
||||
|
||||
let legacy = preview_document_legacy_v1(document).unwrap();
|
||||
assert_eq!(legacy.groups[0].operations[0].category, "items");
|
||||
assert!(
|
||||
!legacy
|
||||
.findings
|
||||
.iter()
|
||||
.any(|finding| finding.code == "invalid_tag")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_response_selection_uses_all_success_codes_then_openapi_wildcard_and_default() {
|
||||
let openapi = r#"
|
||||
openapi: 3.1.0
|
||||
info: { title: Response priority }
|
||||
paths:
|
||||
/numeric:
|
||||
get:
|
||||
operationId: numeric
|
||||
responses:
|
||||
'200': { description: no schema }
|
||||
'203': { description: accepted, content: { application/json: { schema: { type: string } } } }
|
||||
'206': { description: partial, content: { application/json: { schema: { type: integer } } } }
|
||||
/wildcard:
|
||||
get:
|
||||
operationId: wildcard
|
||||
responses:
|
||||
'200': { description: unsupported, content: { text/plain: { schema: { type: string } } } }
|
||||
'2xX': { description: wildcard, content: { application/json: { schema: { type: boolean } } } }
|
||||
default: { description: fallback, content: { application/json: { schema: { type: string } } } }
|
||||
/default:
|
||||
get:
|
||||
operationId: fallback
|
||||
responses:
|
||||
'203': { description: no schema, content: { application/json: {} } }
|
||||
default: { description: fallback, content: { application/json: { schema: { type: number } } } }
|
||||
"#;
|
||||
let ir = normalize_document(openapi, &NormalizationConfig::default()).unwrap();
|
||||
let response_pointer = |path: &str| {
|
||||
ir.operations
|
||||
.iter()
|
||||
.find(|operation| operation.path == path)
|
||||
.unwrap()
|
||||
.response_schema
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.location
|
||||
.pointer
|
||||
.as_str()
|
||||
};
|
||||
assert_eq!(
|
||||
response_pointer("/numeric"),
|
||||
"/paths/~1numeric/get/responses/203/content/application~1json/schema"
|
||||
);
|
||||
assert_eq!(
|
||||
response_pointer("/wildcard"),
|
||||
"/paths/~1wildcard/get/responses/2xX/content/application~1json/schema"
|
||||
);
|
||||
assert_eq!(
|
||||
response_pointer("/default"),
|
||||
"/paths/~1default/get/responses/default/content/application~1json/schema"
|
||||
);
|
||||
|
||||
let swagger = r#"
|
||||
swagger: '2.0'
|
||||
info: { title: Swagger response priority }
|
||||
paths:
|
||||
/numeric:
|
||||
get:
|
||||
operationId: numeric
|
||||
responses:
|
||||
'200': { description: no schema }
|
||||
'206': { description: partial, schema: { type: integer } }
|
||||
default: { description: fallback, schema: { type: string } }
|
||||
/no-wildcard:
|
||||
get:
|
||||
operationId: noWildcard
|
||||
responses:
|
||||
'2XX': { description: ignored wildcard, schema: { type: string } }
|
||||
default: { description: fallback, schema: { type: boolean } }
|
||||
"#;
|
||||
let ir = normalize_document(swagger, &NormalizationConfig::default()).unwrap();
|
||||
let response_pointer = |path: &str| {
|
||||
ir.operations
|
||||
.iter()
|
||||
.find(|operation| operation.path == path)
|
||||
.unwrap()
|
||||
.response_schema
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.location
|
||||
.pointer
|
||||
.as_str()
|
||||
};
|
||||
assert_eq!(
|
||||
response_pointer("/numeric"),
|
||||
"/paths/~1numeric/get/responses/206/schema"
|
||||
);
|
||||
assert_eq!(
|
||||
response_pointer("/no-wildcard"),
|
||||
"/paths/~1no-wildcard/get/responses/default/schema"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schemas_keep_selected_source_pointers_and_pointer_derived_ids() {
|
||||
let openapi = r#"
|
||||
openapi: 3.1.0
|
||||
info: { title: Schema locations }
|
||||
paths:
|
||||
/items:
|
||||
post:
|
||||
operationId: updateItems
|
||||
requestBody:
|
||||
content:
|
||||
application/vnd.example+json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
a/b: { type: string }
|
||||
a~1b: { type: string }
|
||||
responses:
|
||||
'201':
|
||||
description: created
|
||||
content:
|
||||
application/vnd.example+json:
|
||||
schema: { type: integer }
|
||||
"#;
|
||||
let ir = normalize_document(openapi, &NormalizationConfig::default()).unwrap();
|
||||
let operation = &ir.operations[0];
|
||||
assert_eq!(
|
||||
operation
|
||||
.request_body_schema
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.location
|
||||
.pointer,
|
||||
"/paths/~1items/post/requestBody/content/application~1vnd.example+json/schema"
|
||||
);
|
||||
assert_eq!(
|
||||
operation.response_schema.as_ref().unwrap().location.pointer,
|
||||
"/paths/~1items/post/responses/201/content/application~1vnd.example+json/schema"
|
||||
);
|
||||
let crank_import::rest::NormalizedSchemaKind::Object { properties, .. } =
|
||||
&operation.request_body_schema.as_ref().unwrap().kind
|
||||
else {
|
||||
panic!("request schema must be object");
|
||||
};
|
||||
assert_ne!(
|
||||
properties["a/b"].construct_id,
|
||||
properties["a~1b"].construct_id
|
||||
);
|
||||
|
||||
let swagger = r#"
|
||||
swagger: '2.0'
|
||||
info: { title: Swagger schema locations }
|
||||
paths:
|
||||
/items:
|
||||
post:
|
||||
operationId: updateItems
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
schema: { type: object }
|
||||
responses:
|
||||
'201': { description: created, schema: { type: integer } }
|
||||
"#;
|
||||
let ir = normalize_document(swagger, &NormalizationConfig::default()).unwrap();
|
||||
assert_eq!(
|
||||
ir.operations[0]
|
||||
.request_body_schema
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.location
|
||||
.pointer,
|
||||
"/paths/~1items/post/parameters/0/schema"
|
||||
);
|
||||
assert_eq!(
|
||||
ir.operations[0]
|
||||
.response_schema
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.location
|
||||
.pointer,
|
||||
"/paths/~1items/post/responses/201/schema"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_every_unresolved_reference_from_the_full_source_tree() {
|
||||
let document = r#"
|
||||
openapi: 3.1.0
|
||||
info: { title: References }
|
||||
paths:
|
||||
/ok:
|
||||
parameters:
|
||||
- { $ref: '#/components/parameters/Id' }
|
||||
get:
|
||||
operationId: getOk
|
||||
requestBody: { $ref: '#/components/requestBodies/Body' }
|
||||
responses:
|
||||
'200': { $ref: '#/components/responses/Ok' }
|
||||
/reused:
|
||||
$ref: '#/components/pathItems/Reusable'
|
||||
components:
|
||||
schemas:
|
||||
Loop: { $ref: '#/components/schemas/Loop' }
|
||||
Remote: { $ref: 'https://example.test/schema.json#/Remote' }
|
||||
parameters:
|
||||
Id: { $ref: '#/components/parameters/Id' }
|
||||
requestBodies:
|
||||
Body: { $ref: '#/components/requestBodies/Body' }
|
||||
responses:
|
||||
Ok: { $ref: '#/components/responses/Ok' }
|
||||
pathItems:
|
||||
Reusable: { $ref: '#/components/pathItems/Reusable' }
|
||||
"#;
|
||||
let first = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
||||
let second = normalize_document(document, &NormalizationConfig::default()).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_vec(&first).unwrap(),
|
||||
serde_json::to_vec(&second).unwrap()
|
||||
);
|
||||
assert_eq!(first.unresolved_references.len(), 10);
|
||||
let references = first
|
||||
.unresolved_references
|
||||
.iter()
|
||||
.map(|reference| (reference.uri.as_str(), reference.location.pointer.as_str()))
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert!(references.contains(&(
|
||||
"#/components/parameters/Id",
|
||||
"/paths/~1ok/parameters/0/$ref"
|
||||
)));
|
||||
assert!(references.contains(&(
|
||||
"#/components/requestBodies/Body",
|
||||
"/paths/~1ok/get/requestBody/$ref"
|
||||
)));
|
||||
assert!(references.contains(&(
|
||||
"#/components/responses/Ok",
|
||||
"/paths/~1ok/get/responses/200/$ref"
|
||||
)));
|
||||
assert!(references.contains(&("#/components/pathItems/Reusable", "/paths/~1reused/$ref")));
|
||||
assert!(
|
||||
references.contains(&("#/components/schemas/Loop", "/components/schemas/Loop/$ref"))
|
||||
);
|
||||
assert!(references.contains(&(
|
||||
"https://example.test/schema.json#/Remote",
|
||||
"/components/schemas/Remote/$ref"
|
||||
)));
|
||||
assert!(first.unresolved_references.iter().all(|reference| {
|
||||
reference.construct_id
|
||||
== format!(
|
||||
"reference:{}",
|
||||
reference
|
||||
.location
|
||||
.pointer
|
||||
.replace('~', "~0")
|
||||
.replace('/', "~1")
|
||||
)
|
||||
&& first.coverage.iter().any(|entry| {
|
||||
entry.construct_id == reference.construct_id
|
||||
&& entry.location == reference.location
|
||||
})
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user