use itertools::Itertools;
use crate::{
arena::Arena,
ir::{
ContainerView, EdgeKind, EnumVariant, ExtendableView, InlineTypePathRoot,
InlineTypePathSegment, InlineTypeView, ParameterStyle, PrimitiveType, RawGraph, Reach,
RequestView, ResponseView, SchemaTypeInfo, SchemaTypeView, SomeUntaggedVariant, Spec,
StructFieldName, Traversal, TypeView, View,
},
parse::{Document, Method, path::PathFragment},
tests::assert_matches,
};
#[test]
fn test_struct_view_fields_iterator() {
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
age:
type: integer
email:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let person_schema = graph.schemas().find(|s| s.name() == "Person").unwrap();
let person_struct = match person_schema {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Person`; got {other:?}"),
};
let mut field_names = person_struct
.fields()
.map(|f| match f.name() {
StructFieldName::Name(n) => n,
other => panic!("expected explicit struct field name; got {other:?}"),
})
.collect_vec();
field_names.sort();
assert_matches!(&*field_names, ["age", "email", "name"]);
}
#[test]
fn test_struct_field_view_accessors() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Record:
type: object
properties:
id:
type: string
required:
- id
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let record_schema = graph.schemas().find(|s| s.name() == "Record").unwrap();
let record_struct = match record_schema {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Record`; got {other:?}"),
};
let id_field = record_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("id")))
.unwrap();
assert_matches!(id_field.name(), StructFieldName::Name("id"));
assert!(id_field.required());
}
#[test]
fn test_schema_view_from_graph() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
MyStruct:
type: object
properties:
field:
type: string
MyEnum:
type: string
enum: [A, B, C]
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let struct_view = graph.schemas().find(|s| s.name() == "MyStruct").unwrap();
let enum_view = graph.schemas().find(|s| s.name() == "MyEnum").unwrap();
assert_matches!(struct_view, SchemaTypeView::Struct(..));
assert_matches!(enum_view, SchemaTypeView::Enum(..));
}
#[test]
fn test_extension_insertion_retrieval() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
TestStruct:
type: object
properties:
field:
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_view = graph.schemas().next().unwrap();
schema_view.extensions_mut().insert("test_data");
let retrieved = schema_view.extensions().get::<&str>();
assert!(retrieved.is_some());
assert_eq!(*retrieved.unwrap(), "test_data");
}
#[test]
fn test_extension_type_safety() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
TestStruct:
type: object
properties:
field:
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_view = graph.schemas().next().unwrap();
schema_view.extensions_mut().insert("test_string");
let wrong_type = schema_view.extensions().get::<i32>();
assert!(wrong_type.is_none());
let correct_type = schema_view.extensions().get::<&str>();
assert!(correct_type.is_some());
assert_eq!(*correct_type.unwrap(), "test_string");
}
#[test]
fn test_extension_per_node_type() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Struct1:
type: object
required: [field]
properties:
field:
type: string
Struct2:
type: object
required: [field]
properties:
field:
type: string
Struct3:
type: object
required: [ref1, ref2]
properties:
ref1:
$ref: '#/components/schemas/Struct1'
ref2:
$ref: '#/components/schemas/Struct2'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let mut schema1 = graph.schemas().find(|s| s.name() == "Struct1").unwrap();
let mut schema2 = graph.schemas().find(|s| s.name() == "Struct2").unwrap();
schema1.extensions_mut().insert("data_1");
schema2.extensions_mut().insert("data_2");
let ext1 = schema1.extensions().get::<&str>();
let ext2 = schema2.extensions().get::<&str>();
assert!(ext1.is_some());
assert!(ext2.is_some());
assert_eq!(*ext1.unwrap(), "data_1");
assert_eq!(*ext2.unwrap(), "data_2");
let schema3 = graph.schemas().find(|s| s.name() == "Struct3").unwrap();
let struct3_view = match schema3 {
SchemaTypeView::Struct(_, struct_) => struct_,
other => panic!("expected struct `Struct3`; got {other:?}"),
};
let ref1_field = struct3_view
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("ref1")))
.unwrap();
let ref1_ty = ref1_field.ty();
let ref1_schema = match ref1_ty {
TypeView::Schema(schema) => schema,
other => panic!("expected schema reference; got {other:?}"),
};
let ref2_field = struct3_view
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("ref2")))
.unwrap();
let ref2_ty = ref2_field.ty();
let ref2_schema = match ref2_ty {
TypeView::Schema(view) => view,
other => panic!("expected schema reference; got {other:?}"),
};
let ref1_ext = ref1_schema.extensions().get::<&str>();
let ref2_ext = ref2_schema.extensions().get::<&str>();
assert_eq!(*ref1_ext.unwrap(), "data_1");
assert_eq!(*ref2_ext.unwrap(), "data_2");
}
#[test]
fn test_dependencies_multiple() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Leaf1:
type: object
properties:
value:
type: string
Leaf2:
type: object
properties:
data:
type: integer
Branch:
type: object
properties:
leaf1:
$ref: '#/components/schemas/Leaf1'
leaf2:
$ref: '#/components/schemas/Leaf2'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let branch_schema = graph.schemas().find(|s| s.name() == "Branch").unwrap();
let mut dep_names = branch_schema
.dependencies()
.filter_map(|view| match view {
TypeView::Schema(view) => Some(view.name()),
_ => None,
})
.collect_vec();
dep_names.sort();
assert_matches!(&*dep_names, ["Leaf1", "Leaf2"]);
}
#[test]
fn test_dependencies_none() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Standalone:
type: object
required: [field]
properties:
field:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let schema = graph.schemas().next().unwrap();
assert_eq!(schema.dependencies().count(), 1);
}
#[test]
fn test_dependencies_handles_cycles_without_infinite_loop() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
required: [b]
properties:
b:
$ref: '#/components/schemas/B'
B:
type: object
required: [a]
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();
assert_eq!(a_schema.dependencies().count(), 1);
}
#[test]
fn test_dependencies_from_array_includes_inner_types() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Item:
type: object
required: [value]
properties:
value:
type: string
Container:
type: object
required: [items]
properties:
items:
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 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 items_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("items")))
.unwrap();
let container_view = match items_field.ty() {
TypeView::Inline(InlineTypeView::Container(_, view @ ContainerView::Array(_))) => view,
other => panic!("expected inline array; got {other:?}"),
};
let dep_types = container_view.dependencies().collect_vec();
assert_eq!(dep_types.len(), 2);
assert!(dep_types.iter().any(|t| matches!(
t,
TypeView::Schema(SchemaTypeView::Struct(
SchemaTypeInfo { name: "Item", .. },
..
))
)));
assert!(dep_types.iter().any(|t| matches!(
t,
TypeView::Inline(InlineTypeView::Primitive(_, p)) if p.ty() == PrimitiveType::String
)));
}
#[test]
fn test_dependencies_from_map_includes_inner_types() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Item:
type: object
required: [name]
properties:
name:
type: string
Container:
type: object
required: [map_field]
properties:
map_field:
type: object
additionalProperties:
$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 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 map_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("map_field")))
.unwrap();
let container_view = match map_field.ty() {
TypeView::Inline(InlineTypeView::Container(_, view @ ContainerView::Map(_))) => view,
other => panic!("expected inline map; got {other:?}"),
};
let dep_types = container_view.dependencies().collect_vec();
assert_eq!(dep_types.len(), 2);
assert!(dep_types.iter().any(|t| matches!(
t,
TypeView::Schema(SchemaTypeView::Struct(
SchemaTypeInfo { name: "Item", .. },
..
))
)));
assert!(dep_types.iter().any(|t| matches!(
t,
TypeView::Inline(InlineTypeView::Primitive(_, p)) if p.ty() == PrimitiveType::String
)));
}
#[test]
fn test_dependencies_from_nullable_includes_inner_types() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Item:
type: object
nullable: true
required: [value]
properties:
value:
type: string
Container:
type: object
required: [nullable_field]
properties:
nullable_field:
$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 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 nullable_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("nullable_field")))
.unwrap();
let container_view = match nullable_field.ty() {
TypeView::Inline(InlineTypeView::Container(_, view @ ContainerView::Optional(_))) => view,
other => panic!("expected optional; got {other:?}"),
};
let dep_types = container_view.dependencies().collect_vec();
assert_eq!(dep_types.len(), 2);
assert!(dep_types.iter().any(|t| matches!(
t,
TypeView::Schema(SchemaTypeView::Struct(
SchemaTypeInfo { name: "Item", .. },
..
))
)));
assert!(dep_types.iter().any(|t| matches!(
t,
TypeView::Inline(InlineTypeView::Primitive(_, p)) if p.ty() == PrimitiveType::String
)));
}
#[test]
fn test_dependencies_from_inline_includes_inner_types() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
RefSchema:
type: object
required: [value]
properties:
value:
type: string
Container:
type: object
required: [inline_field]
properties:
inline_field:
type: object
required: [ref_field]
properties:
ref_field:
$ref: '#/components/schemas/RefSchema'
"})
.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 inline_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("inline_field")))
.unwrap();
let inline_view = match inline_field.ty() {
TypeView::Inline(view) => view,
other => panic!("expected inline; got {other:?}"),
};
let dep_types = inline_view.dependencies().collect_vec();
assert_eq!(dep_types.len(), 2);
assert!(dep_types.iter().any(|t| matches!(
t,
TypeView::Schema(SchemaTypeView::Struct(
SchemaTypeInfo {
name: "RefSchema",
..
},
..
))
)));
assert!(dep_types.iter().any(|t| matches!(
t,
TypeView::Inline(InlineTypeView::Primitive(_, p)) if p.ty() == PrimitiveType::String
)));
}
#[test]
fn test_dependencies_from_primitive_returns_empty() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Simple:
type: object
required: [name]
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 simple_schema = graph.schemas().next().unwrap();
let simple_struct = match simple_schema {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Simple`; got {other:?}"),
};
let name_field = simple_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("name")))
.unwrap();
let primitive_view = match name_field.ty() {
TypeView::Inline(InlineTypeView::Primitive(_, view)) => view,
other => panic!("expected primitive; got {other:?}"),
};
assert_eq!(primitive_view.dependencies().count(), 0);
}
#[test]
fn test_dependencies_from_any_returns_empty() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Container:
type: object
required: [untyped]
properties:
untyped:
additionalProperties: true
"})
.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().next().unwrap();
let container_struct = match container_schema {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Container`; got {other:?}"),
};
let untyped_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("untyped")))
.unwrap();
let untyped_view = match untyped_field.ty() {
TypeView::Inline(InlineTypeView::Any(_, view)) => view,
other => panic!("expected any; got {other:?}"),
};
assert_eq!(untyped_view.dependencies().count(), 0);
}
#[test]
fn test_traverse_skip_excludes_node_but_continues_traversal() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Leaf:
type: object
properties:
value:
type: string
Middle:
type: object
properties:
leaf:
$ref: '#/components/schemas/Leaf'
Root:
type: object
properties:
middle:
$ref: '#/components/schemas/Middle'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let root = graph.schemas().find(|s| s.name() == "Root").unwrap();
let dep_names = root
.traverse(Reach::Dependencies, |kind, view| {
assert_eq!(kind, EdgeKind::Reference);
match view {
TypeView::Schema(s) if s.name() == "Middle" => Traversal::Skip,
_ => Traversal::Visit,
}
})
.filter_map(|view| match view {
TypeView::Schema(s) => Some(s.name()),
_ => None,
})
.collect_vec();
assert_eq!(dep_names, vec!["Leaf"]);
}
#[test]
fn test_traverse_stop_includes_node_but_stops_traversal() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Leaf:
type: object
properties:
value:
type: string
Middle:
type: object
properties:
leaf:
$ref: '#/components/schemas/Leaf'
Root:
type: object
properties:
middle:
$ref: '#/components/schemas/Middle'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let root = graph.schemas().find(|s| s.name() == "Root").unwrap();
let dep_names = root
.traverse(Reach::Dependencies, |kind, view| {
assert_eq!(kind, EdgeKind::Reference);
match view {
TypeView::Schema(s) if s.name() == "Middle" => Traversal::Stop,
_ => Traversal::Visit,
}
})
.filter_map(|view| match view {
TypeView::Schema(s) => Some(s.name()),
_ => None,
})
.collect_vec();
assert_eq!(dep_names, vec!["Middle"]);
}
#[test]
fn test_traverse_ignore_excludes_node_and_stops_traversal() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Leaf:
type: object
properties:
value:
type: string
Middle:
type: object
properties:
leaf:
$ref: '#/components/schemas/Leaf'
Root:
type: object
properties:
middle:
$ref: '#/components/schemas/Middle'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let root = graph.schemas().find(|s| s.name() == "Root").unwrap();
let dep_names = root
.traverse(Reach::Dependencies, |kind, view| {
assert_eq!(kind, EdgeKind::Reference);
match view {
TypeView::Schema(s) if s.name() == "Middle" => Traversal::Ignore,
_ => Traversal::Visit,
}
})
.filter_map(|view| match view {
TypeView::Schema(s) => Some(s.name()),
_ => None,
})
.collect_vec();
assert!(dep_names.is_empty());
}
#[test]
fn test_traverse_dependents_yields_types_that_depend_on_node() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Leaf:
type: object
properties:
value:
type: string
Middle:
type: object
properties:
leaf:
$ref: '#/components/schemas/Leaf'
Root:
type: object
properties:
middle:
$ref: '#/components/schemas/Middle'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let leaf = graph.schemas().find(|s| s.name() == "Leaf").unwrap();
let dependent_names = leaf
.traverse(Reach::Dependents, |kind, _| {
assert_eq!(kind, EdgeKind::Reference);
Traversal::Visit
})
.filter_map(|view| match view {
TypeView::Schema(s) => Some(s.name()),
_ => None,
})
.collect_vec();
assert_eq!(dependent_names, vec!["Middle", "Root"]);
}
#[test]
fn test_traverse_filter_on_edge_kind() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Leaf:
type: object
properties:
value:
type: string
Parent:
type: object
properties:
id:
type: integer
Child:
allOf:
- $ref: '#/components/schemas/Parent'
type: object
properties:
leaf:
$ref: '#/components/schemas/Leaf'
"})
.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 inherits_only = child
.traverse(Reach::Dependencies, |kind, _| match kind {
EdgeKind::Inherits => Traversal::Visit,
EdgeKind::Reference => Traversal::Ignore,
})
.filter_map(|view| match view {
TypeView::Schema(s) => Some(s.name()),
_ => None,
})
.collect_vec();
assert_eq!(inherits_only, vec!["Parent"]);
let references_only = child
.traverse(Reach::Dependencies, |kind, _| match kind {
EdgeKind::Reference => Traversal::Visit,
EdgeKind::Inherits => Traversal::Ignore,
})
.filter_map(|view| match view {
TypeView::Schema(s) => Some(s.name()),
_ => None,
})
.collect_vec();
assert_eq!(references_only, vec!["Leaf"]);
}
#[test]
fn test_inlines_finds_inline_structs_in_struct_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:
inline_obj:
type: object
properties:
nested_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_schema = graph.schemas().next().unwrap();
assert_eq!(parent_schema.inlines().count(), 4);
}
#[test]
fn test_inlines_finds_inline_types_in_nested_arrays() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Container:
type: object
properties:
items:
type: array
items:
type: object
properties:
item:
type: string
"})
.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().next().unwrap();
assert_eq!(container_schema.inlines().count(), 5);
}
#[test]
fn test_inlines_empty_for_schemas_with_no_inlines() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Simple:
type: object
properties:
field:
$ref: '#/components/schemas/Other'
Other:
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 simple_schema = graph.schemas().find(|s| s.name() == "Simple").unwrap();
assert_eq!(simple_schema.inlines().count(), 1);
}
#[test]
fn test_tagged_variant_names_and_aliases() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
components:
schemas:
Cat:
type: object
properties:
meow:
type: string
Dog:
type: object
properties:
bark:
type: string
Animal:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let animal_schema = graph.schemas().find(|s| s.name() == "Animal").unwrap();
let tagged_view = match animal_schema {
SchemaTypeView::Tagged(_, view) => view,
other => panic!("expected tagged union `Animal`; got {other:?}"),
};
let mut variant_names = tagged_view.variants().map(|v| v.name()).collect_vec();
variant_names.sort();
assert_matches!(&*variant_names, ["Cat", "Dog"]);
let variant = tagged_view.variants().next().unwrap();
assert_eq!(variant.name(), "Cat");
assert_matches!(variant.aliases(), ["cat"]);
}
#[test]
fn test_tagged_variant_type_access() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
components:
schemas:
Cat:
type: object
properties:
meow:
type: string
Animal:
oneOf:
- $ref: '#/components/schemas/Cat'
discriminator:
propertyName: kind
mapping:
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 animal_schema = graph.schemas().find(|s| s.name() == "Animal").unwrap();
let tagged_view = match animal_schema {
SchemaTypeView::Tagged(_, view) => view,
other => panic!("expected tagged union `Animal`; got {other:?}"),
};
let variant = tagged_view.variants().next().unwrap();
let ty = variant.ty();
assert_matches!(ty, TypeView::Schema(view) if view.name() == "Cat");
}
#[test]
fn test_untagged_variant_iteration() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
components:
schemas:
Cat:
type: object
properties:
meow:
type: string
Dog:
type: object
properties:
bark:
type: string
Animal:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let animal_schema = graph.schemas().find(|s| s.name() == "Animal").unwrap();
let untagged_view = match animal_schema {
SchemaTypeView::Untagged(_, view) => view,
_ => panic!("`Animal` should be an untagged union"),
};
assert_eq!(untagged_view.variants().count(), 2);
}
#[test]
fn test_array_view_provides_access_to_item_type() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Container:
type: object
required: [items]
properties:
items:
type: array
items:
type: string
"})
.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().next().unwrap();
let container_struct = match container_schema {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Container`; got {other:?}"),
};
let items_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("items")))
.unwrap();
assert_matches!(
items_field.ty(),
TypeView::Inline(InlineTypeView::Container(_, ContainerView::Array(inner)))
if matches!(
inner.ty(),
TypeView::Inline(InlineTypeView::Primitive(_, p)) if p.ty() == PrimitiveType::String,
),
);
}
#[test]
fn test_map_view_provides_access_to_value_type() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Container:
type: object
required: [map_field]
properties:
map_field:
type: object
additionalProperties:
type: string
"})
.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().next().unwrap();
let container_struct = match container_schema {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Container`; got {other:?}"),
};
let map_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("map_field")))
.unwrap();
assert_matches!(
map_field.ty(),
TypeView::Inline(InlineTypeView::Container(_, ContainerView::Map(inner)))
if matches!(
inner.ty(),
TypeView::Inline(InlineTypeView::Primitive(_, p)) if p.ty() == PrimitiveType::String,
),
);
}
#[test]
fn test_nullable_view_provides_access_to_inner_type() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Container:
type: object
properties:
nullable_field:
type: object
nullable: true
properties:
data:
type: string
"})
.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().next().unwrap();
let container_struct = match container_schema {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Container`; got {other:?}"),
};
let nullable_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("nullable_field")))
.unwrap();
assert_matches!(
nullable_field.ty(),
TypeView::Inline(InlineTypeView::Container(_, ContainerView::Optional(inner)))
if matches!(
inner.ty(),
TypeView::Inline(InlineTypeView::Struct(_, _)),
),
);
}
#[test]
fn test_inline_struct_view_construction_and_path_access() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Container:
type: object
required: [inline_obj]
properties:
inline_obj:
type: object
properties:
nested_field:
type: string
"})
.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().next().unwrap();
let container_struct = match container_schema {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Container`; got {other:?}"),
};
let inline_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("inline_obj")))
.unwrap();
let field_ty = inline_field.ty();
let inline_view = match field_ty {
TypeView::Inline(inline_view) => inline_view,
other => panic!("expected inline type; got {other:?}"),
};
assert_matches!(inline_view, InlineTypeView::Struct(_, _));
let path = inline_view.path();
assert_eq!(path.segments.len(), 1);
}
#[test]
fn test_inline_enum_view_construction() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Container:
type: object
required: [status]
properties:
status:
type: string
enum: [active, inactive, pending]
"})
.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().next().unwrap();
let container_struct = match container_schema {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Container`; got {other:?}"),
};
let status_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("status")))
.unwrap();
let field_ty = status_field.ty();
let inline_view = match field_ty {
TypeView::Inline(inline_view) => inline_view,
other => panic!("expected inline enum; got {other:?}"),
};
let enum_view = match inline_view {
InlineTypeView::Enum(_, view) => view,
other => panic!("expected inline enum; got {other:?}"),
};
assert_eq!(enum_view.variants().len(), 3);
}
#[test]
fn test_inline_untagged_view_construction() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Container:
type: object
required: [value]
properties:
value:
oneOf:
- type: string
- type: integer
"})
.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().next().unwrap();
let container_struct = match container_schema {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Container`; got {other:?}"),
};
let value_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("value")))
.unwrap();
let field_ty = value_field.ty();
let inline_view = match field_ty {
TypeView::Inline(inline_view) => inline_view,
other => panic!("expected inline untagged union; got {other:?}"),
};
let untagged_view = match inline_view {
InlineTypeView::Untagged(_, view) => view,
other => panic!("expected inline untagged union; got {other:?}"),
};
assert_eq!(untagged_view.variants().count(), 2);
}
#[test]
fn test_inline_view_path_method() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Parent:
type: object
required: [nested]
properties:
nested:
type: object
properties:
deep:
type: object
properties:
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_schema = graph.schemas().next().unwrap();
let parent_struct = match parent_schema {
SchemaTypeView::Struct(_, view) => view,
other => panic!("expected struct `Parent`; got {other:?}"),
};
let nested_field = parent_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("nested")))
.unwrap();
let nested_ty = nested_field.ty();
let nested_inline = match nested_ty {
TypeView::Inline(inline_view) => inline_view,
other => panic!("expected inline type; got {other:?}"),
};
let path = nested_inline.path();
assert_matches!(
path.segments,
[InlineTypePathSegment::Field(StructFieldName::Name(
"nested"
))]
);
}
#[test]
fn test_inline_view_with_view_trait_methods() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/records:
post:
operationId: createRecord
requestBody:
content:
application/json:
schema:
type: object
required: [status]
properties:
status:
type: string
enum: [draft, published]
responses:
'201':
description: Created
components:
schemas: {}
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let operation = graph.operations().next().unwrap();
let request = operation.request().unwrap();
let request_ty = match request {
RequestView::Json(ty) => ty,
other => panic!("expected JSON request; got `{other:?}`"),
};
let request_struct = match request_ty {
TypeView::Inline(InlineTypeView::Struct(_, view)) => view,
other => panic!("expected inline struct; got {other:?}"),
};
let status_field = request_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("status")))
.unwrap();
let status_ty = status_field.ty();
let inline_enum = match status_ty {
TypeView::Inline(inline_view) => inline_view,
other => panic!("expected inline type; got {other:?}"),
};
let used_by = inline_enum.used_by().map(|op| op.resource()).collect_vec();
assert_matches!(&*used_by, [None]);
assert_eq!(inline_enum.inlines().count(), 1);
assert_eq!(inline_enum.dependencies().count(), 0);
}
#[test]
fn test_untagged_variant_with_null_type() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
components:
schemas:
Cat:
type: object
properties:
meow:
type: string
Dog:
type: object
properties:
bark:
type: string
Animal:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
- type: 'null'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let animal_schema = graph.schemas().find(|s| s.name() == "Animal").unwrap();
let untagged_view = match animal_schema {
SchemaTypeView::Untagged(_, view) => view,
other => panic!("expected untagged union `Animal`; got {other:?}"),
};
let variants = untagged_view.variants().collect_vec();
assert_eq!(variants.len(), 3);
let cat_variant = &variants[0];
assert_matches!(
cat_variant.ty(),
Some(SomeUntaggedVariant {
view: TypeView::Schema(view),
..
}) if view.name() == "Cat",
);
let dog_variant = &variants[1];
assert_matches!(
dog_variant.ty(),
Some(SomeUntaggedVariant {
view: TypeView::Schema(view),
..
}) if view.name() == "Dog",
);
let null_variant = &variants[2];
assert!(null_variant.ty().is_none());
}
#[test]
fn test_enum_view_variants() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Status:
type: string
enum: [active, inactive, pending]
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let status_schema = graph.schemas().find(|s| s.name() == "Status").unwrap();
let enum_view = match status_schema {
SchemaTypeView::Enum(_, view) => view,
other => panic!("expected enum `Status`; got {other:?}"),
};
let variants = enum_view.variants();
assert_matches!(
variants,
[
EnumVariant::String("active"),
EnumVariant::String("inactive"),
EnumVariant::String("pending"),
]
);
}
#[test]
fn test_enum_view_with_description() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Priority:
type: string
description: Task priority level
enum: [low, medium, high]
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let priority_schema = graph.schemas().find(|s| s.name() == "Priority").unwrap();
let enum_view = match priority_schema {
SchemaTypeView::Enum(_, view) => view,
other => panic!("expected enum `Priority`; got {other:?}"),
};
assert_matches!(enum_view.description(), Some("Task priority level"));
}
#[test]
fn test_enum_view_without_description() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Status:
type: string
enum: [active, inactive]
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let status_schema = graph.schemas().find(|s| s.name() == "Status").unwrap();
let enum_view = match status_schema {
SchemaTypeView::Enum(_, view) => view,
other => panic!("expected enum `Status`; got {other:?}"),
};
assert_matches!(enum_view.description(), None);
}
#[test]
fn test_enum_view_variants_with_numbers() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Priority:
type: integer
enum: [1, 2, 3]
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let priority_schema = graph.schemas().find(|s| s.name() == "Priority").unwrap();
let enum_view = match priority_schema {
SchemaTypeView::Enum(_, view) => view,
other => panic!("expected enum `Priority`; got {other:?}"),
};
let variants = enum_view.variants();
let [
EnumVariant::I64(n1),
EnumVariant::I64(n2),
EnumVariant::I64(n3),
] = variants
else {
panic!("expected 3 variants; got {variants:?}");
};
assert_eq!(*n1, 1);
assert_eq!(*n2, 2);
assert_eq!(*n3, 3);
}
#[test]
fn test_enum_view_variants_with_booleans() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Toggle:
type: boolean
enum: [true, false]
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let toggle_schema = graph.schemas().find(|s| s.name() == "Toggle").unwrap();
let enum_view = match toggle_schema {
SchemaTypeView::Enum(_, view) => view,
other => panic!("expected enum `Toggle`; got {other:?}"),
};
let variants = enum_view.variants();
let &[EnumVariant::Bool(b1), EnumVariant::Bool(b2)] = variants else {
panic!("expected 2 variants; got {variants:?}");
};
assert!(b1);
assert!(!b2);
}
#[test]
fn test_enum_view_with_view_trait_methods() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/tasks:
get:
operationId: getTasks
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
status:
$ref: '#/components/schemas/Status'
components:
schemas:
Status:
type: string
enum: [pending, completed, cancelled]
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let status_schema = graph.schemas().find(|s| s.name() == "Status").unwrap();
let enum_view = match &status_schema {
SchemaTypeView::Enum(_, view) => view,
other => panic!("expected enum `Status`; got {other:?}"),
};
assert_eq!(status_schema.resource(), None);
let used_by = enum_view.used_by().map(|op| op.resource()).collect_vec();
assert_matches!(&*used_by, [None]);
assert_eq!(enum_view.inlines().count(), 0);
assert_eq!(enum_view.dependencies().count(), 0);
}
#[test]
fn test_operation_view_resource() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/users:
get:
operationId: getUsers
x-resource-name: UserResource
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
components:
schemas: {}
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let operation = graph.operations().next().unwrap();
assert_eq!(operation.resource(), Some("UserResource"));
}
#[test]
fn test_operation_view_method() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/users:
get:
operationId: getUsers
responses:
'200':
description: OK
post:
operationId: createUser
responses:
'201':
description: Created
put:
operationId: updateUser
responses:
'200':
description: OK
patch:
operationId: patchUser
responses:
'200':
description: OK
delete:
operationId: deleteUser
responses:
'204':
description: No Content
components:
schemas: {}
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let operations = graph.operations().collect_vec();
assert_eq!(operations.len(), 5);
let get_op = operations.iter().find(|op| op.id() == "getUsers").unwrap();
assert_matches!(get_op.method(), Method::Get);
let post_op = operations
.iter()
.find(|op| op.id() == "createUser")
.unwrap();
assert_matches!(post_op.method(), Method::Post);
let put_op = operations
.iter()
.find(|op| op.id() == "updateUser")
.unwrap();
assert_matches!(put_op.method(), Method::Put);
let patch_op = operations.iter().find(|op| op.id() == "patchUser").unwrap();
assert_matches!(patch_op.method(), Method::Patch);
let delete_op = operations
.iter()
.find(|op| op.id() == "deleteUser")
.unwrap();
assert_matches!(delete_op.method(), Method::Delete);
}
#[test]
fn test_operation_view_inlines_excludes_schema_references() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/users:
get:
operationId: getUsers
responses:
'200':
description: OK
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 operation = graph.operations().next().unwrap();
assert_eq!(operation.inlines().count(), 0);
}
#[test]
fn test_operation_view_inlines_with_mixed_types() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/users:
post:
operationId: createUser
requestBody:
content:
application/json:
schema:
type: object
properties:
profile:
$ref: '#/components/schemas/Profile'
metadata:
type: object
properties:
tags:
type: array
items:
type: string
responses:
'201':
description: Created
components:
schemas:
Profile:
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 operation = graph.operations().next().unwrap();
assert_eq!(operation.inlines().count(), 7);
}
#[test]
fn test_operation_parameter_ty() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/users/{id}:
get:
operationId: getUser
parameters:
- name: id
in: path
required: true
schema:
type: string
- name: tags
in: query
schema:
type: array
items:
type: string
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 operation = graph.operations().next().unwrap();
let path_param = operation.path().params().next().unwrap();
assert_matches!(
path_param.ty(),
TypeView::Inline(InlineTypeView::Primitive(_, p)) if p.ty() == PrimitiveType::String,
);
let query_param = operation.query().next().unwrap();
assert_matches!(
query_param.ty(),
TypeView::Inline(InlineTypeView::Container(_, ContainerView::Array(inner)))
if matches!(
inner.ty(),
TypeView::Inline(InlineTypeView::Primitive(_, p)) if p.ty() == PrimitiveType::String,
),
);
}
#[test]
fn test_operation_parameter_style() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/users:
get:
operationId: listUsers
parameters:
- name: tags
in: query
schema:
type: array
items:
type: string
style: form
explode: false
- name: filters
in: query
schema:
type: array
items:
type: string
style: pipeDelimited
- name: space_separated
in: query
schema:
type: array
items:
type: string
style: spaceDelimited
- name: form_exploded
in: query
schema:
type: array
items:
type: string
style: form
explode: true
- name: deep_obj
in: query
schema:
type: object
style: deepObject
- name: no_style
in: query
schema:
type: string
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 operation = graph.operations().next().unwrap();
let query_params = operation.query().collect_vec();
let tags = query_params.iter().find(|p| p.name() == "tags").unwrap();
assert_matches!(tags.style(), Some(ParameterStyle::Form { exploded: false }),);
let filters = query_params.iter().find(|p| p.name() == "filters").unwrap();
assert_matches!(filters.style(), Some(ParameterStyle::PipeDelimited));
let space = query_params
.iter()
.find(|p| p.name() == "space_separated")
.unwrap();
assert_matches!(space.style(), Some(ParameterStyle::SpaceDelimited));
let form_exploded = query_params
.iter()
.find(|p| p.name() == "form_exploded")
.unwrap();
assert_matches!(
form_exploded.style(),
Some(ParameterStyle::Form { exploded: true }),
);
let deep_obj = query_params
.iter()
.find(|p| p.name() == "deep_obj")
.unwrap();
assert_matches!(deep_obj.style(), Some(ParameterStyle::DeepObject));
let no_style = query_params
.iter()
.find(|p| p.name() == "no_style")
.unwrap();
assert_matches!(no_style.style(), None);
}
#[test]
fn test_operation_request_json() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/users:
post:
operationId: createUser
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
responses:
'201':
description: Created
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let operation = graph.operations().next().unwrap();
assert_matches!(
operation.request(),
Some(RequestView::Json(TypeView::Inline(_))),
);
}
#[test]
fn test_operation_request_multipart() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/upload:
post:
operationId: uploadFile
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
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 operation = graph.operations().next().unwrap();
assert_matches!(operation.request(), Some(RequestView::Multipart));
}
#[test]
fn test_operation_path() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/users/{id}:
get:
operationId: getUser
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
id:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let operation = graph.operations().next().unwrap();
let segments = operation.path().segments().as_slice();
let [a, b] = segments else {
panic!("expected two path segments; got {segments:?}");
};
assert_matches!(a.fragments(), [PathFragment::Literal("users")],);
assert_matches!(b.fragments(), [PathFragment::Param("id")]);
}
#[test]
fn test_operation_response_without_schema() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/resource:
get:
operationId: getResource
responses:
'200':
description: OK
content:
application/json:
schema: {}
components:
schemas: {}
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let operation = graph.operations().next().unwrap();
assert_matches!(
operation.response(),
Some(ResponseView::Json(TypeView::Inline(InlineTypeView::Any(
..
))))
);
}
#[test]
fn test_operation_query() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/users:
get:
operationId: listUsers
parameters:
- name: limit
in: query
required: true
schema:
type: integer
- name: offset
in: query
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let operation = graph.operations().next().unwrap();
let query_params = operation.query().collect_vec();
let [limit, offset] = &*query_params else {
panic!("expected two query parameters; got {query_params:?}");
};
assert_eq!(limit.name(), "limit");
assert!(limit.required());
assert_eq!(offset.name(), "offset");
assert!(!offset.required());
}
#[test]
fn test_operation_view_inlines_finds_inline_types() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/users:
post:
operationId: createUser
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
address:
type: object
properties:
street:
type: string
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
properties:
id:
type: string
components:
schemas: {}
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let operation = graph.operations().next().unwrap();
let inlines = operation.inlines().collect_vec();
assert_eq!(inlines.len(), 10);
let address = inlines
.iter()
.find(|inline| {
matches!(inline, InlineTypeView::Struct(path, _) if matches!(
path.segments,
[
InlineTypePathSegment::Operation("createUser"),
InlineTypePathSegment::Request,
InlineTypePathSegment::Field(StructFieldName::Name("address")),
],
))
})
.unwrap();
assert_matches!(address, InlineTypeView::Struct(_, _));
let request = inlines
.iter()
.find(|inline| {
matches!(inline, InlineTypeView::Struct(path, _) if matches!(
path.segments,
[
InlineTypePathSegment::Operation("createUser"),
InlineTypePathSegment::Request,
],
))
})
.unwrap();
assert_matches!(request, InlineTypeView::Struct(_, _));
let response = inlines
.iter()
.find(|inline| {
matches!(inline, InlineTypeView::Struct(path, _) if matches!(
path.segments,
[
InlineTypePathSegment::Operation("createUser"),
InlineTypePathSegment::Response,
],
))
})
.unwrap();
assert_matches!(response, InlineTypeView::Struct(_, _));
}
#[test]
fn test_operation_request_and_response() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0
paths:
/users:
post:
operationId: createUser
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
properties:
id:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let operation = graph.operations().next().unwrap();
let Some(RequestView::Json(TypeView::Inline(request))) = operation.request() else {
panic!(
"expected inline request schema; got {:?}",
operation.request(),
);
};
assert_matches!(request.path().root, InlineTypePathRoot::Resource(None));
assert_matches!(
request.path().segments,
[
InlineTypePathSegment::Operation("createUser"),
InlineTypePathSegment::Request,
],
);
let Some(ResponseView::Json(TypeView::Inline(response))) = operation.response() else {
panic!(
"expected inline response schema; got {:?}",
operation.response(),
);
};
assert_matches!(response.path().root, InlineTypePathRoot::Resource(None));
assert_matches!(
response.path().segments,
[
InlineTypePathSegment::Operation("createUser"),
InlineTypePathSegment::Response,
],
);
}
#[test]
fn test_variant_field_matching_tagged_union_tag_is_tag() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Post:
oneOf:
- $ref: '#/components/schemas/Comment'
- $ref: '#/components/schemas/Reaction'
discriminator:
propertyName: kind
mapping:
comment: '#/components/schemas/Comment'
reaction: '#/components/schemas/Reaction'
Comment:
type: object
required: [kind, id, text]
properties:
kind:
type: string
id:
type: string
text:
type: string
Reaction:
type: object
required: [kind, id, emoji]
properties:
kind:
type: string
id:
type: string
emoji:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let comment = graph.schemas().find(|s| s.name() == "Comment").unwrap();
let SchemaTypeView::Struct(_, comment_struct) = comment else {
panic!("expected struct `Comment`; got `{comment:?}`");
};
let kind_field = comment_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("kind")))
.unwrap();
assert!(kind_field.tag());
let id_field = comment_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("id")))
.unwrap();
assert!(!id_field.tag());
let reaction = graph.schemas().find(|s| s.name() == "Reaction").unwrap();
let SchemaTypeView::Struct(_, reaction_struct) = reaction else {
panic!("expected struct `Reaction`; got `{reaction:?}`");
};
let kind_field = reaction_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("kind")))
.unwrap();
assert!(kind_field.tag());
}
#[test]
fn test_transitive_dependency_field_matching_tag_is_not_tag() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Outer:
oneOf:
- $ref: '#/components/schemas/Wrapper'
discriminator:
propertyName: kind
mapping:
wrapper: '#/components/schemas/Wrapper'
Wrapper:
type: object
required: [kind, data]
properties:
kind:
type: string
data:
$ref: '#/components/schemas/Inner'
Inner:
type: object
properties:
kind:
type: string
value:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = RawGraph::new(&arena, &spec).cook();
let wrapper = graph.schemas().find(|s| s.name() == "Wrapper").unwrap();
let SchemaTypeView::Struct(_, wrapper_struct) = wrapper else {
panic!("expected struct `Wrapper`; got `{wrapper:?}`");
};
let kind_field = wrapper_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("kind")))
.unwrap();
assert!(kind_field.tag());
let inner = graph.schemas().find(|s| s.name() == "Inner").unwrap();
let SchemaTypeView::Struct(_, inner_struct) = inner else {
panic!("expected struct `Inner`; got `{inner:?}`");
};
let kind_field = inner_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("kind")))
.unwrap();
assert!(!kind_field.tag());
}
#[test]
fn test_own_struct_tag_field() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Base:
type: object
properties:
kind:
type: string
name:
type: string
discriminator:
propertyName: kind
Container:
oneOf:
- $ref: '#/components/schemas/Base'
discriminator:
propertyName: kind
mapping:
base: '#/components/schemas/Base'
"})
.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 SchemaTypeView::Struct(_, base_struct) = base else {
panic!("expected struct `Base`; got `{base:?}`");
};
let kind_field = base_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("kind")))
.unwrap();
assert!(kind_field.tag());
let name_field = base_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("name")))
.unwrap();
assert!(!name_field.tag());
}
#[test]
fn test_inherited_tag_field() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Parent:
type: object
properties:
kind:
type: string
discriminator:
propertyName: kind
Child:
allOf:
- $ref: '#/components/schemas/Parent'
properties:
name:
type: string
Container:
oneOf:
- $ref: '#/components/schemas/Child'
discriminator:
propertyName: kind
mapping:
child: '#/components/schemas/Child'
"})
.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 SchemaTypeView::Struct(_, child_struct) = child else {
panic!("expected struct `Child`; got `{child:?}`");
};
let kind_field = child_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("kind")))
.unwrap();
assert!(kind_field.tag());
assert!(kind_field.inherited());
let name_field = child_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("name")))
.unwrap();
assert!(!name_field.tag());
assert!(!name_field.inherited());
}
#[test]
fn test_fields_linearizes_inline_all_of_parents() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Person:
allOf:
- type: object
properties:
name:
type: string
- type: object
properties:
age:
type: integer
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 person = graph.schemas().find(|s| s.name() == "Person").unwrap();
let SchemaTypeView::Struct(_, person_struct) = person else {
panic!("expected struct `Person`; got `{person:?}`");
};
let field_names = person_struct
.fields()
.filter_map(|f| match f.name() {
StructFieldName::Name(n) => Some(n),
_ => None,
})
.collect_vec();
assert_eq!(field_names, vec!["name", "age", "email"]);
let name_field = person_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("name")))
.unwrap();
assert!(name_field.inherited());
let age_field = person_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("age")))
.unwrap();
assert!(age_field.inherited());
let email_field = person_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("email")))
.unwrap();
assert!(!email_field.inherited());
}
#[test]
fn test_inline_tagged_view_construction() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Cat:
type: object
properties:
meow:
type: string
Dog:
type: object
properties:
bark:
type: string
Container:
type: object
required: [animal]
properties:
animal:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
"})
.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 animal_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("animal")))
.unwrap();
let field_ty = animal_field.ty();
let inline_view = match field_ty {
TypeView::Inline(inline_view) => inline_view,
other => panic!("expected inline tagged union; got {other:?}"),
};
let tagged_view = match inline_view {
InlineTypeView::Tagged(_, view) => view,
other => panic!("expected inline tagged union; got {other:?}"),
};
assert_eq!(tagged_view.tag(), "kind");
let mut variant_names = tagged_view.variants().map(|v| v.name()).collect_vec();
variant_names.sort();
assert_matches!(&*variant_names, ["Cat", "Dog"]);
}
#[test]
fn test_inline_tagged_view_variant_types() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Cat:
type: object
properties:
meow:
type: string
Container:
type: object
required: [animal]
properties:
animal:
oneOf:
- $ref: '#/components/schemas/Cat'
discriminator:
propertyName: kind
mapping:
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 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 animal_field = container_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("animal")))
.unwrap();
let inline_view = match animal_field.ty() {
TypeView::Inline(inline_view) => inline_view,
other => panic!("expected inline type; got {other:?}"),
};
let tagged_view = match inline_view {
InlineTypeView::Tagged(_, view) => view,
other => panic!("expected inline tagged union; got {other:?}"),
};
let variant = tagged_view.variants().next().unwrap();
assert_eq!(variant.name(), "Cat");
assert_matches!(variant.ty(), TypeView::Schema(view) if view.name() == "Cat");
}
#[test]
fn test_inlines_finds_inline_tagged_unions() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Cat:
type: object
properties:
meow:
type: string
Dog:
type: object
properties:
bark:
type: string
Container:
type: object
properties:
animal:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
"})
.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 inlines = container_schema.inlines().collect_vec();
assert_matches!(
&*inlines,
[
InlineTypeView::Container(_, _),
InlineTypeView::Tagged(_, _)
]
);
}
#[test]
fn test_tag_false_for_inlined_struct() {
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
bark:
type: string
Pet:
oneOf:
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
dog: '#/components/schemas/Dog'
Owner:
type: object
properties:
dog:
$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 dog = graph.schemas().find(|s| s.name() == "Dog").unwrap();
let SchemaTypeView::Struct(_, dog_struct) = dog else {
panic!("expected struct `Dog`; got `{dog:?}`");
};
let kind_field = dog_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("kind")))
.unwrap();
assert!(!kind_field.tag());
}
#[test]
fn test_inlined_when_tagged_unions_disagree_on_tag() {
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
category:
type: string
bark:
type: string
ByKind:
oneOf:
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
dog: '#/components/schemas/Dog'
ByCategory:
oneOf:
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: category
mapping:
dog: '#/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 dog = graph.schemas().find(|s| s.name() == "Dog").unwrap();
let SchemaTypeView::Struct(_, dog_struct) = dog else {
panic!("expected struct `Dog`; got `{dog:?}`");
};
let field_names = dog_struct.fields().map(|f| f.name()).collect_vec();
assert_matches!(
&*field_names,
[
StructFieldName::Name("kind"),
StructFieldName::Name("category"),
StructFieldName::Name("bark"),
]
);
let by_kind = graph.schemas().find(|s| s.name() == "ByKind").unwrap();
let SchemaTypeView::Tagged(_, by_kind_tagged) = by_kind else {
panic!("expected tagged `ByKind`; got `{by_kind:?}`");
};
let variant = by_kind_tagged.variants().next().unwrap();
let TypeView::Inline(InlineTypeView::Struct(_, inline_struct)) = variant.ty() else {
panic!("expected inline struct variant; got `{:?}`", variant.ty());
};
let inline_fields = inline_struct.fields().map(|f| f.name()).collect_vec();
assert_matches!(
&*inline_fields,
[
StructFieldName::Name("kind"),
StructFieldName::Name("category"),
StructFieldName::Name("bark"),
]
);
let tags = inline_struct
.fields()
.filter(|f| f.tag())
.map(|f| f.name())
.collect_vec();
assert_matches!(&*tags, [StructFieldName::Name("kind")]);
let by_category = graph.schemas().find(|s| s.name() == "ByCategory").unwrap();
let SchemaTypeView::Tagged(_, by_category_tagged) = by_category else {
panic!("expected tagged `ByCategory`; got `{by_category:?}`");
};
let variant = by_category_tagged.variants().next().unwrap();
let TypeView::Inline(InlineTypeView::Struct(_, inline_struct)) = variant.ty() else {
panic!("expected inline struct variant; got `{:?}`", variant.ty());
};
let inline_fields = inline_struct.fields().map(|f| f.name()).collect_vec();
assert_matches!(
&*inline_fields,
[
StructFieldName::Name("kind"),
StructFieldName::Name("category"),
StructFieldName::Name("bark"),
]
);
let tags = inline_struct
.fields()
.filter(|f| f.tag())
.map(|f| f.name())
.collect_vec();
assert_matches!(&*tags, [StructFieldName::Name("category")]);
}
#[test]
fn test_inlined_when_tagged_unions_disagree_on_fields() {
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
bark:
type: string
UnionA:
oneOf:
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
dog: '#/components/schemas/Dog'
properties:
kind:
type: string
name:
type: string
UnionB:
oneOf:
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
dog: '#/components/schemas/Dog'
properties:
kind:
type: string
habitat:
type: string
"})
.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 dog = graph.schemas().find(|s| s.name() == "Dog").unwrap();
let SchemaTypeView::Struct(_, dog_struct) = dog else {
panic!("expected struct `Dog`; got `{dog:?}`");
};
let field_names = dog_struct.fields().map(|f| f.name()).collect_vec();
assert_matches!(
&*field_names,
[StructFieldName::Name("kind"), StructFieldName::Name("bark")]
);
let union_a = graph.schemas().find(|s| s.name() == "UnionA").unwrap();
let SchemaTypeView::Tagged(_, union_a_tagged) = union_a else {
panic!("expected tagged `UnionA`; got `{union_a:?}`");
};
let variant = union_a_tagged.variants().next().unwrap();
let TypeView::Inline(InlineTypeView::Struct(_, inline_struct)) = variant.ty() else {
panic!("expected inline struct variant; got `{:?}`", variant.ty());
};
let inline_fields = inline_struct.fields().map(|f| f.name()).collect_vec();
assert_matches!(
&*inline_fields,
[
StructFieldName::Name("name"),
StructFieldName::Name("kind"),
StructFieldName::Name("bark"),
]
);
let union_b = graph.schemas().find(|s| s.name() == "UnionB").unwrap();
let SchemaTypeView::Tagged(_, union_b_tagged) = union_b else {
panic!("expected tagged `UnionB`; got `{union_b:?}`");
};
let variant = union_b_tagged.variants().next().unwrap();
let TypeView::Inline(InlineTypeView::Struct(_, inline_struct)) = variant.ty() else {
panic!("expected inline struct variant; got `{:?}`", variant.ty());
};
let inline_fields = inline_struct.fields().map(|f| f.name()).collect_vec();
assert_matches!(
&*inline_fields,
[
StructFieldName::Name("habitat"),
StructFieldName::Name("kind"),
StructFieldName::Name("bark"),
]
);
}
#[test]
fn test_not_inlined_when_variant_already_inherits_union_fields() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Dog:
allOf:
- $ref: '#/components/schemas/Pet'
properties:
bark:
type: string
Pet:
oneOf:
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
dog: '#/components/schemas/Dog'
properties:
kind:
type: string
name:
type: string
Owner:
type: object
properties:
dog:
$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();
assert_matches!(variant.ty(), TypeView::Schema(SchemaTypeView::Struct(..)));
let dog = graph.schemas().find(|s| s.name() == "Dog").unwrap();
let SchemaTypeView::Struct(_, dog_struct) = dog else {
panic!("expected struct `Dog`; got `{dog:?}`");
};
let field_names = dog_struct.fields().map(|f| f.name()).collect_vec();
assert_matches!(
&*field_names,
[
StructFieldName::Name("kind"),
StructFieldName::Name("name"),
StructFieldName::Name("bark"),
]
);
}
#[test]
fn test_inlining_preserves_field_type_edges() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
paths: {}
components:
schemas:
Dog:
type: object
properties:
kind:
type: string
bark:
type: string
Pet:
oneOf:
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
dog: '#/components/schemas/Dog'
properties:
severity:
type: string
enum:
- low
- high
Owner:
type: object
properties:
dog:
$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 has_inline_enum = pet.inlines().any(|i| matches!(i, InlineTypeView::Enum(..)));
assert!(has_inline_enum);
}
#[test]
fn test_inlined_variant_inline_field_types_not_leaked() {
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
details:
type: object
properties:
color:
type: string
Pet:
oneOf:
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
dog: '#/components/schemas/Dog'
Owner:
type: object
properties:
dog:
$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 pet_inlines = pet.inlines().collect_vec();
let [InlineTypeView::Struct(path, _)] = &*pet_inlines else {
panic!("expected inline struct variant `Dog`; got `{pet_inlines:?}`");
};
assert_matches!(path.root, InlineTypePathRoot::Type("Pet"));
assert_matches!(path.segments, [InlineTypePathSegment::TaggedVariant("Dog")]);
let dog = graph.schemas().find(|s| s.name() == "Dog").unwrap();
let dog_inlines = dog.inlines().collect_vec();
assert!(
dog_inlines
.iter()
.any(|i| matches!(i, InlineTypeView::Struct(..))),
"expected `Dog` to have inline struct `Details`"
);
assert!(
dog_inlines
.iter()
.all(|i| i.path().root == InlineTypePathRoot::Type("Dog")),
"all of `Dog`'s inlines should be rooted at `Dog`"
);
}
#[test]
fn test_tag_false_when_only_operation_prevents_inlining() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
paths:
/dogs:
get:
operationId: getDog
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Dog'
components:
schemas:
Dog:
type: object
properties:
kind:
type: string
bark:
type: string
Pet:
oneOf:
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
dog: '#/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 dog = graph.schemas().find(|s| s.name() == "Dog").unwrap();
let SchemaTypeView::Struct(_, dog_struct) = dog else {
panic!("expected struct `Dog`; got `{dog:?}`");
};
let kind_field = dog_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("kind")))
.unwrap();
assert!(
!kind_field.tag(),
"`kind` should not be a tag on the schema struct \
when an operation references it"
);
}
#[test]
fn test_inlined_when_struct_field_references_tagged_variant() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
paths: {}
components:
schemas:
Dog:
type: object
properties:
kind:
type: string
bark:
type: string
required:
- kind
- bark
Owner:
type: object
properties:
dog:
$ref: '#/components/schemas/Dog'
Pet:
oneOf:
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
dog: '#/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 dog = graph.schemas().find(|s| s.name() == "Dog").unwrap();
let SchemaTypeView::Struct(_, dog_struct) = dog else {
panic!("expected struct `Dog`; got `{dog:?}`");
};
let kind_field = dog_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("kind")))
.unwrap();
assert!(
!kind_field.tag(),
"`kind` should not be a tag on the schema struct \
when a non-tagged schema also references it"
);
let pet = graph.schemas().find(|s| s.name() == "Pet").unwrap();
let SchemaTypeView::Tagged(_, pet_tagged) = pet else {
panic!("expected tagged `Pet`; got `{pet:?}`");
};
let variant = pet_tagged.variants().next().unwrap();
let TypeView::Inline(InlineTypeView::Struct(path, inline_struct)) = variant.ty() else {
panic!("expected inline struct variant; got `{:?}`", variant.ty());
};
assert_matches!(path.root, InlineTypePathRoot::Type("Pet"));
assert_matches!(path.segments, [InlineTypePathSegment::TaggedVariant("Dog")]);
let inline_fields = inline_struct.fields().map(|f| f.name()).collect_vec();
assert_matches!(
&*inline_fields,
[StructFieldName::Name("kind"), StructFieldName::Name("bark"),]
);
let kind_inline = inline_struct
.fields()
.find(|f| matches!(f.name(), StructFieldName::Name("kind")))
.unwrap();
assert!(
kind_inline.tag(),
"`kind` should be a tag on the inlined struct variant"
);
}