Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/schema-recursive-refs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---

Fix stack overflow when resolving recursive schema references, preserving non-cyclic fields and leaving cyclic references intact.
158 changes: 153 additions & 5 deletions crates/google-workspace-cli/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ fn resolve_schema_refs(
) {
match val {
Value::Object(map) => {
let mut expanded_ref = None;
// Check if this object is a reference
if let Some(ref_name) = map
.get("$ref")
Expand All @@ -288,18 +289,15 @@ fn resolve_schema_refs(
if !seen.contains(&ref_name) {
if let Some(schema) = doc.schemas.get(&ref_name) {
seen.insert(ref_name.clone());
let mut resolved = schema_to_json(schema);
// Recursively resolve the resolved schema
resolve_schema_refs(&mut resolved, doc, seen);
seen.remove(&ref_name);

let resolved = schema_to_json(schema);
// Merge resolved schema into current object, but preserve existing fields
// (though usually $ref stands alone)
if let Value::Object(resolved_map) = resolved {
for (k, v) in resolved_map {
map.entry(k).or_insert(v);
}
}
expanded_ref = Some(ref_name);
}
}
}
Expand All @@ -308,6 +306,10 @@ fn resolve_schema_refs(
for (_, v) in map.iter_mut() {
resolve_schema_refs(v, doc, seen);
}

if let Some(ref_name) = expanded_ref {
seen.remove(&ref_name);
}
}
Value::Array(arr) => {
for v in arr {
Expand Down Expand Up @@ -465,4 +467,150 @@ mod tests {
let f_node = &val["properties"]["f"];
assert_eq!(f_node["type"], "integer");
}

#[test]
fn test_resolve_schema_refs_self_recursive_array_preserves_fields() {
let doc = RestDescription {
schemas: serde_json::from_value(json!({
"Part": {
"type": "object",
"properties": {
"filename": { "type": "string" },
"parts": { "type": "array", "items": { "$ref": "Part" } },
"body": { "$ref": "Body" }
}
},
"Body": {
"type": "object",
"properties": { "data": { "type": "string" } }
}
}))
.unwrap(),
..Default::default()
};
let mut val = json!({ "$ref": "Part" });

resolve_schema_refs(&mut val, &doc, &mut std::collections::HashSet::new());

assert_eq!(
val,
json!({
"$ref": "Part",
"type": "object",
"properties": {
"filename": { "type": "string" },
"parts": { "type": "array", "items": { "$ref": "Part" } },
"body": {
"$ref": "Body",
"type": "object",
"properties": { "data": { "type": "string" } }
}
}
})
);
}

#[test]
fn test_resolve_schema_refs_mutual_cycle_preserves_fields() {
let doc = RestDescription {
schemas: serde_json::from_value(json!({
"First": {
"type": "object",
"properties": { "next": { "$ref": "Second" } }
},
"Second": {
"type": "object",
"properties": {
"previous": { "$ref": "First" },
"name": { "type": "string" }
}
}
}))
.unwrap(),
..Default::default()
};
let mut val = json!({ "$ref": "First" });

resolve_schema_refs(&mut val, &doc, &mut std::collections::HashSet::new());

assert_eq!(
val["properties"]["next"],
json!({
"$ref": "Second",
"type": "object",
"properties": {
"previous": { "$ref": "First" },
"name": { "type": "string" }
}
})
);
}

#[test]
fn test_resolve_schema_refs_sibling_branches_preserve_overrides() {
let doc = RestDescription {
schemas: serde_json::from_value(json!({
"Target": { "type": "string", "description": "Schema description" }
}))
.unwrap(),
..Default::default()
};
let mut val = json!([
{ "$ref": "Target", "description": "Field description" },
{ "$ref": "Target" },
{ "$ref": "Missing", "description": "Unknown schema" }
]);

resolve_schema_refs(&mut val, &doc, &mut std::collections::HashSet::new());

assert_eq!(
val,
json!([
{
"$ref": "Target",
"type": "string",
"description": "Field description"
},
{
"$ref": "Target",
"type": "string",
"description": "Schema description"
},
{ "$ref": "Missing", "description": "Unknown schema" }
])
);
}

#[test]
fn test_resolve_schema_refs_schema_lookup_preserves_non_cyclic_fields() {
let doc = RestDescription {
schemas: serde_json::from_value(json!({
"Part": {
"type": "object",
"properties": {
"child": { "$ref": "Part" },
"body": { "$ref": "Body" }
}
},
"Body": { "type": "string" }
}))
.unwrap(),
..Default::default()
};
let mut val = schema_to_json(&doc.schemas["Part"]);
let mut seen = std::collections::HashSet::from(["Part".to_string()]);

resolve_schema_refs(&mut val, &doc, &mut seen);

assert_eq!(
val,
json!({
"type": "object",
"properties": {
"child": { "$ref": "Part" },
"body": { "$ref": "Body", "type": "string" }
}
})
);
}
}
Loading