use itertools::Itertools;
use crate::{
arena::Arena,
ir::{InlineTypeView, RawGraph, SchemaTypeView, Spec, StructFieldName, TypeView, View},
parse::Document,
tests::assert_matches,
};
#[test]
fn test_graph_basic_construction() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Person:
type: object
properties:
name:
type: string
Company:
type: object
properties:
title:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let mut schema_names = graph.schemas().map(|s| s.name()).collect_vec();
schema_names.sort();
assert_matches!(&*schema_names, ["Company", "Person"]);
}
#[test]
fn test_graph_deduplication() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Shared:
type: object
properties:
id:
type: string
Container1:
type: object
properties:
value:
$ref: '#/components/schemas/Shared'
Container2:
type: object
properties:
value:
$ref: '#/components/schemas/Shared'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let mut schema_names = graph.schemas().map(|s| s.name()).collect_vec();
schema_names.sort();
assert_matches!(&*schema_names, ["Container1", "Container2", "Shared"]);
}
#[test]
fn test_graph_struct_field_edges() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
FieldType:
type: object
properties:
value:
type: string
Container:
type: object
properties:
field:
$ref: '#/components/schemas/FieldType'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let mut schema_names = graph.schemas().map(|s| s.name()).collect_vec();
schema_names.sort();
assert_matches!(&*schema_names, ["Container", "FieldType"]);
}
#[test]
fn test_graph_tagged_variant_edges() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Dog:
type: object
properties:
bark:
type: string
Cat:
type: object
properties:
meow:
type: string
Animal:
oneOf:
- $ref: '#/components/schemas/Dog'
- $ref: '#/components/schemas/Cat'
discriminator:
propertyName: type
mapping:
dog: '#/components/schemas/Dog'
cat: '#/components/schemas/Cat'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let mut schema_names = graph.schemas().map(|s| s.name()).collect_vec();
schema_names.sort();
assert_matches!(&*schema_names, ["Animal", "Cat", "Dog"]);
}
#[test]
fn test_graph_untagged_variant_edges() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
TypeA:
type: object
properties:
a:
type: string
TypeB:
type: object
properties:
b:
type: integer
AOrB:
oneOf:
- $ref: '#/components/schemas/TypeA'
- $ref: '#/components/schemas/TypeB'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let mut schema_names = graph.schemas().map(|s| s.name()).collect_vec();
schema_names.sort();
assert_matches!(&*schema_names, ["AOrB", "TypeA", "TypeB"]);
}
#[test]
fn test_graph_array_edge() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Item:
type: object
properties:
value:
type: string
Items:
type: object
properties:
list:
type: array
items:
$ref: '#/components/schemas/Item'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let mut schema_names = graph.schemas().map(|s| s.name()).collect_vec();
schema_names.sort();
assert_matches!(&*schema_names, ["Item", "Items"]);
}
#[test]
fn test_graph_map_edge() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Value:
type: object
properties:
data:
type: string
Dictionary:
type: object
properties:
map:
type: object
additionalProperties:
$ref: '#/components/schemas/Value'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let mut schema_names = graph.schemas().map(|s| s.name()).collect_vec();
schema_names.sort();
assert_matches!(&*schema_names, ["Dictionary", "Value"]);
}
#[test]
fn test_graph_nullable_edge() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
NullableType:
type: object
nullable: true
properties:
value:
type: string
Container:
type: object
properties:
field:
$ref: '#/components/schemas/NullableType'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let mut schema_names = graph.schemas().map(|s| s.name()).collect_vec();
schema_names.sort();
assert_matches!(&*schema_names, ["Container", "NullableType"]);
}
#[test]
fn test_graph_ref_resolution() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Child:
type: object
properties:
name:
type: string
Parent:
type: object
properties:
child1:
$ref: '#/components/schemas/Child'
child2:
$ref: '#/components/schemas/Child'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let mut schema_names = graph.schemas().map(|s| s.name()).collect_vec();
schema_names.sort();
assert_matches!(&*schema_names, ["Child", "Parent"]);
}
#[test]
fn test_circular_refs_simple_cycle() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
properties:
b:
$ref: '#/components/schemas/B'
B:
type: object
properties:
a:
$ref: '#/components/schemas/A'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let a_schema = graph.schemas().find(|s| s.name() == "A").unwrap();
let a_struct = match a_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `A`; got {other:?}"),
};
let b_field = a_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("b")))
.unwrap();
assert!(b_field.needs_indirection());
}
#[test]
fn test_circular_refs_self_reference() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Node:
type: object
properties:
next:
$ref: '#/components/schemas/Node'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let node_schema = graph.schemas().find(|s| s.name() == "Node").unwrap();
let node_struct = match node_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `Node`; got {other:?}"),
};
let next_field = node_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("next")))
.unwrap();
assert!(next_field.needs_indirection());
}
#[test]
fn test_circular_refs_complex_cycle() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
properties:
b:
$ref: '#/components/schemas/B'
B:
type: object
properties:
c:
$ref: '#/components/schemas/C'
C:
type: object
properties:
a:
$ref: '#/components/schemas/A'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let a_schema = graph.schemas().find(|s| s.name() == "A").unwrap();
let a_struct = match a_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `A`; got {other:?}"),
};
let b_field = a_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("b")))
.unwrap();
assert!(b_field.needs_indirection());
}
#[test]
fn test_circular_refs_no_cycles() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Leaf:
type: string
Branch:
type: object
properties:
leaf:
$ref: '#/components/schemas/Leaf'
Root:
type: object
properties:
branch:
$ref: '#/components/schemas/Branch'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let root_schema = graph.schemas().find(|s| s.name() == "Root").unwrap();
let root_struct = match root_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `Root`; got {other:?}"),
};
let branch_field = root_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("branch")))
.unwrap();
assert!(!branch_field.needs_indirection());
}
#[test]
fn test_circular_refs_multiple_sccs() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
properties:
b:
$ref: '#/components/schemas/B'
B:
type: object
properties:
a:
$ref: '#/components/schemas/A'
C:
type: object
properties:
d:
$ref: '#/components/schemas/D'
D:
type: object
properties:
c:
$ref: '#/components/schemas/C'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let a_schema = graph.schemas().find(|s| s.name() == "A").unwrap();
let a_struct = match a_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `A`; got {other:?}"),
};
let a_b_field = a_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("b")))
.unwrap();
assert!(a_b_field.needs_indirection());
let c_schema = graph.schemas().find(|s| s.name() == "C").unwrap();
let c_struct = match c_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `C`; got {other:?}"),
};
let c_d_field = c_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("d")))
.unwrap();
assert!(c_d_field.needs_indirection());
}
#[test]
fn test_circular_refs_through_containers() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
properties:
b_array:
type: array
items:
$ref: '#/components/schemas/B'
B:
type: object
properties:
a_map:
type: object
additionalProperties:
$ref: '#/components/schemas/A'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let a_schema = graph.schemas().find(|s| s.name() == "A").unwrap();
let a_struct = match a_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `A`; got {other:?}"),
};
let b_array_field = a_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("b_array")))
.unwrap();
assert!(b_array_field.needs_indirection());
}
#[test]
fn test_circular_refs_diamond_no_false_positive() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Base:
type: string
Left:
type: object
properties:
base:
$ref: '#/components/schemas/Base'
Right:
type: object
properties:
base:
$ref: '#/components/schemas/Base'
Top:
type: object
properties:
left:
$ref: '#/components/schemas/Left'
right:
$ref: '#/components/schemas/Right'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let top_schema = graph.schemas().find(|s| s.name() == "Top").unwrap();
let top_struct = match top_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `Top`; got {other:?}"),
};
let left_field = top_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("left")))
.unwrap();
assert!(!left_field.needs_indirection());
}
#[test]
fn test_circular_refs_tarjan_correctness() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
properties:
b:
$ref: '#/components/schemas/B'
B:
type: object
properties:
c:
$ref: '#/components/schemas/C'
a:
$ref: '#/components/schemas/A'
C:
type: object
properties:
d:
$ref: '#/components/schemas/D'
D:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let a_schema = graph.schemas().find(|s| s.name() == "A").unwrap();
let a_struct = match a_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `A`; got {other:?}"),
};
let a_b_field = a_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("b")))
.unwrap();
assert!(a_b_field.needs_indirection());
let c_schema = graph.schemas().find(|s| s.name() == "C").unwrap();
let c_struct = match c_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `C`; got {other:?}"),
};
let c_d_field = c_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("d")))
.unwrap();
assert!(!c_d_field.needs_indirection());
}
#[test]
fn test_needs_indirection_through_nullable() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
nullable: true
properties:
b:
$ref: '#/components/schemas/B'
B:
type: object
nullable: true
properties:
a:
$ref: '#/components/schemas/A'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let a_schema = graph.schemas().find(|s| s.name() == "A").unwrap();
let a_struct = match a_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `A`; got {other:?}"),
};
let b_field = a_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("b")))
.unwrap();
assert!(b_field.needs_indirection());
}
#[test]
fn test_needs_indirection_through_array() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Node:
type: object
properties:
children:
type: array
items:
$ref: '#/components/schemas/Node'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let node_schema = graph.schemas().find(|s| s.name() == "Node").unwrap();
let node_struct = match node_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `Node`; got {other:?}"),
};
let children_field = node_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("children")))
.unwrap();
assert!(children_field.needs_indirection());
}
#[test]
fn test_needs_indirection_through_map() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Node:
type: object
properties:
children_map:
type: object
additionalProperties:
$ref: '#/components/schemas/Node'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let node_schema = graph.schemas().find(|s| s.name() == "Node").unwrap();
let node_struct = match node_schema {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `Node`; got {other:?}"),
};
let children_map_field = node_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("children_map")))
.unwrap();
assert!(children_map_field.needs_indirection());
}
#[test]
fn test_indirect_and_direct_siblings() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Direct:
type: object
properties:
value:
type: string
Container:
type: object
properties:
direct_field:
$ref: '#/components/schemas/Direct'
indirect_field:
$ref: '#/components/schemas/Container'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let container_schema = graph.schemas().find(|s| s.name() == "Container").unwrap();
let container_struct = match container_schema {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Container`; got {other:?}"),
};
let direct_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("direct_field")))
.unwrap();
let indirect_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("indirect_field")))
.unwrap();
assert!(!direct_field.needs_indirection());
assert!(indirect_field.needs_indirection());
}
#[test]
fn test_operations_transitive() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/users:
get:
operationId: getUsers
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/UserList'
components:
schemas:
UserList:
type: object
properties:
users:
type: array
items:
$ref: '#/components/schemas/User'
User:
type: object
properties:
name:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let user_list = graph.schemas().find(|s| s.name() == "UserList").unwrap();
let user = graph.schemas().find(|s| s.name() == "User").unwrap();
assert_eq!(user_list.resource(), None);
let user_list_used_by = user_list.used_by().map(|op| op.resource()).collect_vec();
assert_matches!(&*user_list_used_by, [None]);
assert_eq!(user.resource(), None);
let user_used_by = user.used_by().map(|op| op.resource()).collect_vec();
assert_matches!(&*user_used_by, [None]);
}
#[test]
fn test_operations_multiple() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/users:
get:
operationId: getUsers
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/User'
post:
operationId: createUser
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
name:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let user = graph.schemas().find(|s| s.name() == "User").unwrap();
assert_eq!(user.resource(), None);
let user_used_by = user.used_by().map(|op| op.resource()).collect_vec();
assert_matches!(&*user_used_by, [None, None]);
}
#[test]
fn test_dependencies_propagation() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test API
version: 1.0
paths:
/data:
get:
operationId: getData
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/Response'
/users/{id}:
get:
operationId: getUser
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
Response:
type: object
properties:
user:
$ref: '#/components/schemas/User'
items:
type: array
items:
$ref: '#/components/schemas/Item'
metadata:
type: object
additionalProperties:
$ref: '#/components/schemas/Meta'
User:
type: object
x-resourceId: users
properties:
name:
type: string
Item:
type: object
properties:
id:
type: string
Meta:
type: object
properties:
key:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let get_data = graph.operations().find(|o| o.id() == "getData").unwrap();
let mut get_data_deps = get_data
.dependencies()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
get_data_deps.sort();
assert_matches!(&*get_data_deps, ["Item", "Meta", "Response", "User"]);
let get_user = graph.operations().find(|o| o.id() == "getUser").unwrap();
let get_user_deps = get_user
.dependencies()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
assert_matches!(&*get_user_deps, ["User"]);
let user = graph.schemas().find(|s| s.name() == "User").unwrap();
assert_eq!(user.resource(), Some("users"));
let response = graph.schemas().find(|s| s.name() == "Response").unwrap();
assert_eq!(response.resource(), None);
let mut user_used_by = user.used_by().map(|op| op.id()).collect_vec();
user_used_by.sort();
assert_matches!(&*user_used_by, ["getData", "getUser"]);
let mut other_used_by = graph
.schemas()
.filter(|s| ["Response", "Item", "Meta"].contains(&s.name()))
.flat_map(|schema| schema.used_by())
.map(|op| op.id())
.collect_vec();
other_used_by.dedup();
assert_matches!(&*other_used_by, ["getData"]);
}
#[test]
fn test_used_by_propagation() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
paths:
/cats:
get:
operationId: getCat
x-resource-name: cats
parameters:
- name: options
in: query
schema:
$ref: '#/components/schemas/CreateOptions'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/Cat'
/items:
post:
operationId: createItem
x-resource-name: items
parameters:
- name: options
in: query
schema:
$ref: '#/components/schemas/CreateOptions'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/Parent'
components:
schemas:
Cat:
type: object
properties:
pet:
$ref: '#/components/schemas/Pet'
Pet:
type: object
properties:
cat:
$ref: '#/components/schemas/Cat'
Parent:
type: object
properties:
child:
$ref: '#/components/schemas/Child'
Child:
type: object
x-resourceId: children
properties:
name:
type: string
CreateOptions:
type: object
x-resourceId: options
properties:
verbose:
type: boolean
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let cat = graph.schemas().find(|s| s.name() == "Cat").unwrap();
let pet = graph.schemas().find(|s| s.name() == "Pet").unwrap();
let cat_used_by = cat.used_by().map(|op| op.resource()).collect_vec();
assert_matches!(&*cat_used_by, [Some("cats")]);
let pet_used_by = pet.used_by().map(|op| op.resource()).collect_vec();
assert_matches!(&*pet_used_by, [Some("cats")]);
let child = graph.schemas().find(|s| s.name() == "Child").unwrap();
assert_eq!(child.resource(), Some("children"));
let child_used_by = child.used_by().map(|op| op.resource()).collect_vec();
assert_matches!(&*child_used_by, [Some("items")]);
let options = graph
.schemas()
.find(|s| s.name() == "CreateOptions")
.unwrap();
assert_eq!(options.resource(), Some("options"));
let mut options_used_by = options.used_by().map(|op| op.resource()).collect_vec();
options_used_by.sort();
assert_matches!(&*options_used_by, [Some("cats"), Some("items")]);
let op = graph.operations().find(|o| o.id() == "createItem").unwrap();
let mut op_resources = op
.dependencies()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.resource())
.collect_vec();
op_resources.sort();
assert_matches!(&*op_resources, [None, Some("children"), Some("options")]);
}
#[test]
fn test_depends_on_simple_chain() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
properties:
b:
$ref: '#/components/schemas/B'
B:
type: object
properties:
c:
$ref: '#/components/schemas/C'
C:
type: object
properties:
value:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let a = graph.schemas().find(|s| s.name() == "A").unwrap();
let b = graph.schemas().find(|s| s.name() == "B").unwrap();
let c = graph.schemas().find(|s| s.name() == "C").unwrap();
assert!(a.depends_on(&b));
assert!(a.depends_on(&c));
assert!(b.depends_on(&c));
assert!(!b.depends_on(&a));
assert!(!c.depends_on(&a));
assert!(!c.depends_on(&b));
}
#[test]
fn test_depends_on_cycle() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
properties:
b:
$ref: '#/components/schemas/B'
B:
type: object
properties:
c:
$ref: '#/components/schemas/C'
C:
type: object
properties:
a:
$ref: '#/components/schemas/A'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let a = graph.schemas().find(|s| s.name() == "A").unwrap();
let b = graph.schemas().find(|s| s.name() == "B").unwrap();
let c = graph.schemas().find(|s| s.name() == "C").unwrap();
assert!(a.depends_on(&b));
assert!(a.depends_on(&c));
assert!(b.depends_on(&a));
assert!(b.depends_on(&c));
assert!(c.depends_on(&a));
assert!(c.depends_on(&b));
}
#[test]
fn test_depends_on_independent() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
properties:
value:
type: string
B:
type: object
properties:
value:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let a = graph.schemas().find(|s| s.name() == "A").unwrap();
let b = graph.schemas().find(|s| s.name() == "B").unwrap();
assert!(!a.depends_on(&b));
assert!(!b.depends_on(&a));
}
#[test]
fn test_dependents_simple_chain() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
properties:
b:
$ref: '#/components/schemas/B'
B:
type: object
properties:
c:
$ref: '#/components/schemas/C'
C:
type: object
properties:
value:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let c = graph.schemas().find(|s| s.name() == "C").unwrap();
let mut c_dependents = c
.dependents()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
c_dependents.sort();
assert_matches!(&*c_dependents, ["A", "B"]);
let b = graph.schemas().find(|s| s.name() == "B").unwrap();
let mut b_dependents = b
.dependents()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
b_dependents.sort();
assert_matches!(&*b_dependents, ["A"]);
let a = graph.schemas().find(|s| s.name() == "A").unwrap();
let a_dependents = a
.dependents()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
assert!(a_dependents.is_empty());
}
#[test]
fn test_dependents_multiple_dependents() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
properties:
c:
$ref: '#/components/schemas/C'
B:
type: object
properties:
c:
$ref: '#/components/schemas/C'
C:
type: object
properties:
value:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let c = graph.schemas().find(|s| s.name() == "C").unwrap();
let mut c_dependents = c
.dependents()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
c_dependents.sort();
assert_matches!(&*c_dependents, ["A", "B"]);
}
#[test]
fn test_dependents_cycle() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
properties:
b:
$ref: '#/components/schemas/B'
B:
type: object
properties:
c:
$ref: '#/components/schemas/C'
C:
type: object
properties:
a:
$ref: '#/components/schemas/A'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let a = graph.schemas().find(|s| s.name() == "A").unwrap();
let mut a_dependents = a
.dependents()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
a_dependents.sort();
assert_matches!(&*a_dependents, ["B", "C"]);
let b = graph.schemas().find(|s| s.name() == "B").unwrap();
let mut b_dependents = b
.dependents()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
b_dependents.sort();
assert_matches!(&*b_dependents, ["A", "C"]);
let c = graph.schemas().find(|s| s.name() == "C").unwrap();
let mut c_dependents = c
.dependents()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
c_dependents.sort();
assert_matches!(&*c_dependents, ["A", "B"]);
}
#[test]
fn test_dependents_is_inverse_of_dependencies() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Container:
type: object
properties:
item:
$ref: '#/components/schemas/Item'
Item:
type: object
properties:
value:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let container = graph.schemas().find(|s| s.name() == "Container").unwrap();
let item = graph.schemas().find(|s| s.name() == "Item").unwrap();
let container_deps = container
.dependencies()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
assert_matches!(&*container_deps, ["Item"]);
let mut item_dependents = item
.dependents()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
item_dependents.sort();
assert_matches!(&*item_dependents, ["Container"]);
}
#[test]
fn test_dependencies_diamond() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
properties:
b:
$ref: '#/components/schemas/B'
c:
$ref: '#/components/schemas/C'
B:
type: object
properties:
d:
$ref: '#/components/schemas/D'
C:
type: object
properties:
d:
$ref: '#/components/schemas/D'
D:
type: object
properties:
value:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let a = graph.schemas().find(|s| s.name() == "A").unwrap();
let b = graph.schemas().find(|s| s.name() == "B").unwrap();
let c = graph.schemas().find(|s| s.name() == "C").unwrap();
let d = graph.schemas().find(|s| s.name() == "D").unwrap();
let mut a_deps = a
.dependencies()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
a_deps.sort();
assert_matches!(&*a_deps, ["B", "C", "D"]);
let mut d_dependents = d
.dependents()
.filter_map(|v| v.into_schema().ok())
.map(|s| s.name())
.collect_vec();
d_dependents.sort();
assert_matches!(&*d_dependents, ["A", "B", "C"]);
assert!(b.depends_on(&d));
assert!(c.depends_on(&d));
assert!(!b.depends_on(&c));
assert!(!c.depends_on(&b));
}
#[test]
fn test_operation_with_no_types() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
paths:
/health:
get:
operationId: healthCheck
x-resource-name: health
responses:
'200':
description: OK
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let op = graph
.operations()
.find(|o| o.id() == "healthCheck")
.unwrap();
let deps = op
.dependencies()
.filter_map(|v| v.into_schema().ok())
.collect_vec();
assert_matches!(&*deps, []);
assert!(graph.schemas().all(|schema| {
schema
.used_by()
.map(|op| op.id())
.all(|id| id != "healthCheck")
}));
}
#[test]
fn test_parents_returns_immediate_parents() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Entity:
type: object
properties:
id:
type: string
NamedEntity:
allOf:
- $ref: '#/components/schemas/Entity'
properties:
name:
type: string
User:
allOf:
- $ref: '#/components/schemas/NamedEntity'
properties:
email:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let user = graph.schemas().find(|s| s.name() == "User").unwrap();
let user_struct = match user {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `User`; got {other:?}"),
};
let parent_names = user_struct
.parents()
.filter_map(|p| p.into_schema().ok())
.map(|s| s.name())
.collect_vec();
assert_matches!(&*parent_names, ["NamedEntity"]);
let field_names = user_struct
.fields()
.map(|f| match f.name() {
StructFieldName::Name(n) => n,
other => panic!("expected named field; got {other:?}"),
})
.collect_vec();
assert_matches!(&*field_names, ["id", "name", "email"]);
}
#[test]
fn test_all_of_inheritance_with_fields() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Parent:
type: object
properties:
parent_field:
type: string
Child:
allOf:
- $ref: '#/components/schemas/Parent'
properties:
child_field:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let child = graph.schemas().find(|s| s.name() == "Child").unwrap();
let child_struct = match child {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Child`; got {other:?}"),
};
let parent_names = child_struct
.parents()
.filter_map(|p| p.into_schema().ok())
.map(|s| s.name())
.collect_vec();
assert_matches!(&*parent_names, ["Parent"]);
let own_field_names = child_struct
.own_fields()
.map(|f| match f.name() {
StructFieldName::Name(n) => n,
other => panic!("expected named field; got {other:?}"),
})
.collect_vec();
assert_matches!(&*own_field_names, ["child_field"]);
let all_field_names = child_struct
.fields()
.map(|f| match f.name() {
StructFieldName::Name(n) => n,
_ => panic!("expected named field"),
})
.collect_vec();
assert_matches!(&*all_field_names, ["parent_field", "child_field"]);
let parent_field = child_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("parent_field")))
.unwrap();
assert!(parent_field.inherited());
let child_field = child_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("child_field")))
.unwrap();
assert!(!child_field.inherited());
}
#[test]
fn test_circular_refs_excludes_inherits_edges() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Parent:
type: object
properties:
child:
$ref: '#/components/schemas/Child'
Child:
allOf:
- $ref: '#/components/schemas/Parent'
properties:
own_field:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let parent = graph.schemas().find(|s| s.name() == "Parent").unwrap();
let parent_struct = match parent {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Parent`; got {other:?}"),
};
let child_field = parent_struct
.own_fields()
.find(|f| matches!(f.name(), StructFieldName::Name("child")))
.unwrap();
assert!(!child_field.needs_indirection());
}
#[test]
fn test_multiple_parents() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Mixin1:
type: object
properties:
alpha:
type: string
beta:
type: string
Mixin2:
type: object
properties:
gamma:
type: string
delta:
type: string
Combined:
allOf:
- $ref: '#/components/schemas/Mixin1'
- $ref: '#/components/schemas/Mixin2'
properties:
own_field:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let combined = graph.schemas().find(|s| s.name() == "Combined").unwrap();
let combined_struct = match combined {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Combined`; got {other:?}"),
};
let parent_names = combined_struct
.parents()
.filter_map(|p| p.into_schema().ok())
.map(|s| s.name())
.collect_vec();
assert_matches!(&*parent_names, ["Mixin1", "Mixin2"]);
let own_field_names = combined_struct
.own_fields()
.map(|f| match f.name() {
StructFieldName::Name(n) => n,
other => panic!("expected named field; got {other:?}"),
})
.collect_vec();
assert_matches!(&*own_field_names, ["own_field"]);
let all_field_names = combined_struct
.fields()
.map(|f| match f.name() {
StructFieldName::Name(n) => n,
other => panic!("expected named field; got {other:?}"),
})
.collect_vec();
assert_matches!(
&*all_field_names,
["alpha", "beta", "gamma", "delta", "own_field"]
);
}
#[test]
fn test_circular_all_of_terminates() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
allOf:
- $ref: '#/components/schemas/B'
properties:
a_field:
type: string
B:
allOf:
- $ref: '#/components/schemas/A'
properties:
b_field:
type: string
kind:
type: string
discriminator:
propertyName: kind
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let a = graph.schemas().find(|s| s.name() == "A").unwrap();
let a_struct = match a {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `A`; got {other:?}"),
};
let field_names = a_struct
.fields()
.map(|f| match f.name() {
StructFieldName::Name(n) => n,
other => panic!("expected named field; got {other:?}"),
})
.collect_vec();
assert_matches!(&*field_names, ["b_field", "kind", "a_field"]);
let kind_field = a_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("kind")))
.unwrap();
assert!(!kind_field.tag());
assert!(kind_field.inherited());
let a_field = a_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("a_field")))
.unwrap();
assert!(!a_field.tag());
}
#[test]
fn test_all_of_parent_with_one_of_and_properties() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Column:
type: object
properties:
name:
type: string
Base:
oneOf:
- $ref: '#/components/schemas/VariantA'
- $ref: '#/components/schemas/VariantB'
discriminator:
propertyName: kind
mapping:
a: '#/components/schemas/VariantA'
b: '#/components/schemas/VariantB'
properties:
schema:
type: array
items:
$ref: '#/components/schemas/Column'
kind:
type: string
VariantA:
allOf:
- $ref: '#/components/schemas/Base'
properties:
a_field:
type: string
VariantB:
allOf:
- $ref: '#/components/schemas/Base'
properties:
b_field:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let variant_a = graph.schemas().find(|s| s.name() == "VariantA").unwrap();
let variant_a_struct = match variant_a {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `VariantA`; got {other:?}"),
};
let all_field_names = variant_a_struct
.fields()
.map(|f| match f.name() {
StructFieldName::Name(n) => n,
other => panic!("expected named field; got {other:?}"),
})
.collect_vec();
assert_matches!(&*all_field_names, ["schema", "kind", "a_field"]);
}
#[test]
fn test_untagged_union_with_properties() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Base:
oneOf:
- $ref: '#/components/schemas/VariantA'
- $ref: '#/components/schemas/VariantB'
properties:
shared_field:
type: string
count:
type: integer
VariantA:
allOf:
- $ref: '#/components/schemas/Base'
properties:
a_field:
type: string
VariantB:
allOf:
- $ref: '#/components/schemas/Base'
properties:
b_field:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let base = graph.schemas().find(|s| s.name() == "Base").unwrap();
let base_untagged = match base {
SchemaTypeView::Untagged(_, view) => view,
other => panic!("expected untagged `Base`; got `{other:?}`"),
};
let base_field_names = base_untagged
.fields()
.iter()
.map(|f| match f.name {
StructFieldName::Name(n) => n,
other => panic!("expected named field; got `{other:?}`"),
})
.collect_vec();
assert_matches!(&*base_field_names, ["shared_field", "count"]);
let variant_a = graph.schemas().find(|s| s.name() == "VariantA").unwrap();
let variant_a_struct = match variant_a {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `VariantA`; got `{other:?}`"),
};
let variant_a_field_names = variant_a_struct
.fields()
.map(|f| match f.name() {
StructFieldName::Name(n) => n,
other => panic!("expected named field; got `{other:?}`"),
})
.collect_vec();
assert_matches!(
&*variant_a_field_names,
["shared_field", "count", "a_field"]
);
}
#[test]
fn test_tagged_union_inlines_include_field_types() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Dog:
type: object
properties:
kind:
type: string
Pet:
oneOf:
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
dog: '#/components/schemas/Dog'
properties:
kind:
type: string
severity:
type: string
enum: [low, high]
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let pet = graph.schemas().find(|s| s.name() == "Pet").unwrap();
let has_inline_enum = pet.inlines().any(|i| matches!(i, InlineTypeView::Enum(..)));
assert!(has_inline_enum);
}
#[test]
fn test_needs_indirection_through_inlined_tagged_variant() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Pet:
oneOf:
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
properties:
kind:
type: string
name:
type: string
Dog:
type: object
properties:
parent:
$ref: '#/components/schemas/Pet'
Kennel:
type: object
properties:
resident:
$ref: '#/components/schemas/Dog'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let mut raw = RawGraph::new(&arena, &spec);
raw.inline_tagged_variants();
let graph = raw.cook();
let pet = graph.schemas().find(|s| s.name() == "Pet").unwrap();
let tagged = match pet {
SchemaTypeView::Tagged(_, view) => view,
other => panic!("expected tagged `Pet`; got {other:?}"),
};
let variant = tagged.variants().next().unwrap();
let variant_struct = match variant.ty() {
TypeView::Inline(InlineTypeView::Struct(_, view)) => view,
other => panic!("expected inline struct variant; got {other:?}"),
};
let parent_field = variant_struct
.own_fields()
.find(|f| matches!(f.name(), StructFieldName::Name("parent")))
.unwrap();
assert!(parent_field.needs_indirection());
}