Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,6 @@ impl Compare {
old_schema: Contextual<'_, &Schema>,
new_schema: Contextual<'_, &Schema>,
) -> anyhow::Result<bool> {
// We wait for both new and old to contain a cycle; this ensures that
// we consider "unrolled" cycles properly. There is a possibility of
// getting stuck in an A->B->A / B->A->B cycle... we can address that
// should that construction arise.
if old_schema.context().stack().contains_cycle()
&& new_schema.context().stack().contains_cycle()
{
Expand Down
321 changes: 321 additions & 0 deletions tests/cases/cycle-detection/base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
{
"openapi": "3.0.0",
"info": {
"title": "Cycle detection test fixture",
"version": "1.0.0",
"description": "Exercises cycle detection across a variety of schema shapes."
},
"paths": {
"/items": {
"get": {
"operationId": "list_items",
"responses": {
"200": {
"description": "A page of items",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ItemPage"
}
}
}
}
}
}
},
"/persons": {
"get": {
"operationId": "list_persons",
"responses": {
"200": {
"description": "Persons; reaches Company through a mutual reference",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Person"
}
}
}
}
}
}
},
"/companies": {
"get": {
"operationId": "list_companies",
"responses": {
"200": {
"description": "Companies; reaches Person through a mutual reference",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Company"
}
}
}
}
}
}
},
"/three-cycle": {
"get": {
"operationId": "get_three_cycle",
"responses": {
"200": {
"description": "Three-way cycle: NodeA -> NodeB -> NodeC -> NodeA",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NodeA"
}
}
}
}
}
}
},
"/wrapped-cycle": {
"get": {
"operationId": "get_wrapped_cycle",
"responses": {
"200": {
"description": "Mutual recursion routed through allOf wrappers",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Wrapper"
}
}
}
}
}
}
},
"/direct-loop": {
"get": {
"operationId": "get_direct_loop",
"responses": {
"200": {
"description": "Direct self-cycle; patches may replace this with an unrolled cycle through an intermediate schema",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DirectLoop"
}
}
}
}
}
}
},
"/self-cycle": {
"get": {
"operationId": "get_self_cycle",
"responses": {
"200": {
"description": "Self-cycle through SelfCycleA; patches may replace this with SelfCycleB to exercise pair-keyed cycle detection across different-named tops",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SelfCycleA"
}
}
}
}
}
}
},
"/alternating-cycle": {
"get": {
"operationId": "get_alternating_cycle",
"responses": {
"200": {
"description": "Mutual cycle AltX <-> AltY. Swapping the entry from AltX to AltY makes old-side traversal walk AltX -> AltY -> AltX -> ... while new-side walks AltY -> AltX -> AltY -> ..., exercising the alternating (A,B)/(B,A) pair-keyed traversal.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AltX"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"ItemPage": {
"description": "A page of items",
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Item"
}
}
},
"required": ["items"]
},
"Item": {
"description": "An item",
"type": "object",
"properties": {
"value": {
"type": "string"
}
},
"required": ["value"]
},
"Person": {
"description": "A person who works at a company.",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"employer": {
"$ref": "#/components/schemas/Company"
}
},
"required": ["name"]
},
"Company": {
"description": "A company with a CEO who is a person.",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"ceo": {
"$ref": "#/components/schemas/Person"
}
},
"required": ["name"]
},
"NodeA": {
"description": "First node of a three-way cycle.",
"type": "object",
"properties": {
"label": {
"type": "string"
},
"next": {
"$ref": "#/components/schemas/NodeB"
}
}
},
"NodeB": {
"description": "Second node of a three-way cycle.",
"type": "object",
"properties": {
"label": {
"type": "string"
},
"next": {
"$ref": "#/components/schemas/NodeC"
}
}
},
"NodeC": {
"description": "Third node of a three-way cycle.",
"type": "object",
"properties": {
"label": {
"type": "string"
},
"next": {
"$ref": "#/components/schemas/NodeA"
}
}
},
"Wrapper": {
"description": "Cycles back to Wrapped through an allOf indirection.",
"type": "object",
"properties": {
"value": {
"type": "string"
},
"child": {
"allOf": [
{
"$ref": "#/components/schemas/Wrapped"
}
]
}
}
},
"Wrapped": {
"description": "Cycles back to Wrapper through an allOf indirection.",
"type": "object",
"properties": {
"value": {
"type": "string"
},
"back": {
"allOf": [
{
"$ref": "#/components/schemas/Wrapper"
}
]
}
}
},
"DirectLoop": {
"description": "Direct self-cycle for the asymmetric-unroll test.",
"type": "object",
"properties": {
"value": {
"type": "string"
},
"next": {
"$ref": "#/components/schemas/DirectLoop"
}
}
},
"SelfCycleA": {
"description": "Self-recursive schema A; paired with SelfCycleB to test pair-keyed cycle detection across different-named tops.",
"type": "object",
"properties": {
"value": {
"type": "string"
},
"next": {
"$ref": "#/components/schemas/SelfCycleA"
}
}
},
"SelfCycleB": {
"description": "Self-recursive schema B; structurally similar to SelfCycleA but distinct so a swap exercises (A, B) pair traversal.",
"type": "object",
"properties": {
"value": {
"type": "string"
},
"next": {
"$ref": "#/components/schemas/SelfCycleB"
}
}
},
"AltX": {
"description": "Mutual cycle pair, side X. Structurally identical to AltY (object whose `next` refers to the partner); distinguishable only by name and this description.",
"type": "object",
"properties": {
"next": {
"$ref": "#/components/schemas/AltY"
}
}
},
"AltY": {
"description": "Mutual cycle pair, side Y. Structurally identical to AltX (object whose `next` refers to the partner); distinguishable only by name and this description.",
"type": "object",
"properties": {
"next": {
"$ref": "#/components/schemas/AltX"
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--- add-property-to-mutual-recursion.json
+++ patched
@@
"Person": {
"description": "A person who works at a company.",
"properties": {
+ "age": {
+ "type": "integer"
+ },
"employer": {
"$ref": "#/components/schemas/Company"
},


Result for patch:
[
Change {
message: "object properties changed",
old_path: [
"#/components/schemas/Person",
"#/components/schemas/Company/properties/ceo/$ref",
"#/paths/~1companies/get/responses/200/content/application~1json/schema/$ref",
],
new_path: [
"#/components/schemas/Person",
"#/components/schemas/Company/properties/ceo/$ref",
"#/paths/~1companies/get/responses/200/content/application~1json/schema/$ref",
],
comparison: Output,
class: Unhandled,
details: UnknownDifference,
},
]
Loading
Loading