11use model:: SchemaRef ;
22use serde_json:: Value ;
3- use std:: collections:: HashSet ;
43use std:: path:: Path ;
54
65/// Synthetic URI we register the original OpenAPI document under so the
@@ -63,7 +62,7 @@ pub fn resolve(
6362 } => {
6463 let doc = load_json_or_yaml ( base_dir, openapi) ?;
6564 let schema = extract_component ( & doc, component) ?;
66- let mut resolved = inline_refs ( schema, & doc , & mut HashSet :: new ( ) ) ;
65+ let mut resolved = rewrite_refs ( schema) ;
6766 if * strict {
6867 apply_strict ( & mut resolved) ;
6968 }
@@ -78,60 +77,36 @@ fn extract_component(doc: &Value, name: &str) -> Result<Value, SchemaError> {
7877 . ok_or_else ( || SchemaError :: ComponentNotFound ( name. to_string ( ) ) )
7978}
8079
81- /// Walk `schema`, replacing `{"$ref": "#/components/schemas/X"}` with the
82- /// resolved subschema from `root`. Tracks visited refs to break cycles; on a
83- /// cycle the `$ref` object is preserved but rewritten to point at the
84- /// synthetic root URI (`urn:ace:openapi-root#/...`) so the `jsonschema`
85- /// crate can resolve it against the registered root document at compile
86- /// time. Without that rewrite, the bare `#/components/schemas/X` would
87- /// resolve relative to the extracted standalone schema (which has no
88- /// `components` section) and fail to compile.
89- fn inline_refs ( schema : Value , root : & Value , visiting : & mut HashSet < String > ) -> Value {
80+ /// Walk `schema` and rewrite every local `$ref` (`#/...`) to the synthetic
81+ /// root URI (`urn:ace:openapi-root#/...`). This is O(nodes) with no
82+ /// recursion explosion — we never expand refs inline. The jsonschema
83+ /// compiler resolves them against the root document registered via
84+ /// `with_document`. Replacing the old `inline_refs` expansion avoids
85+ /// blowing up on large specs (e.g. Stripe's 7 MB OpenAPI file) where full
86+ /// inlining produces hundreds of MB of JSON.
87+ fn rewrite_refs ( schema : Value ) -> Value {
9088 match schema {
9189 Value :: Object ( mut map) => {
9290 if let Some ( Value :: String ( ref_str) ) = map. get ( "$ref" ) {
93- let ref_str = ref_str. clone ( ) ;
94- if let Some ( ptr) = local_ref_to_pointer ( & ref_str) {
95- if visiting. contains ( & ref_str) {
96- // Cycle: rewrite to the synthetic root URI so the
97- // jsonschema compiler can resolve it via the
98- // registered root document.
99- map. insert (
100- "$ref" . to_string ( ) ,
101- Value :: String ( format ! ( "{SYNTHETIC_OPENAPI_ROOT_URI}{ref_str}" ) ) ,
102- ) ;
103- return Value :: Object ( map) ;
104- }
105- if let Some ( target) = root. pointer ( & ptr) {
106- visiting. insert ( ref_str. clone ( ) ) ;
107- let inlined = inline_refs ( target. clone ( ) , root, visiting) ;
108- visiting. remove ( & ref_str) ;
109- return inlined;
110- }
91+ if ref_str. starts_with ( '#' ) {
92+ let rewritten =
93+ format ! ( "{SYNTHETIC_OPENAPI_ROOT_URI}{ref_str}" ) ;
94+ map. insert ( "$ref" . to_string ( ) , Value :: String ( rewritten) ) ;
11195 }
112- // Non-local ref or pointer not found — leave as-is.
11396 return Value :: Object ( map) ;
11497 }
11598 for val in map. values_mut ( ) {
116- * val = inline_refs ( val. take ( ) , root , visiting ) ;
99+ * val = rewrite_refs ( val. take ( ) ) ;
117100 }
118101 Value :: Object ( map)
119102 }
120- Value :: Array ( arr) => Value :: Array (
121- arr. into_iter ( )
122- . map ( |v| inline_refs ( v, root, visiting) )
123- . collect ( ) ,
124- ) ,
103+ Value :: Array ( arr) => {
104+ Value :: Array ( arr. into_iter ( ) . map ( rewrite_refs) . collect ( ) )
105+ }
125106 other => other,
126107 }
127108}
128109
129- /// Convert a local JSON Reference like `#/components/schemas/Foo` to a
130- /// JSON Pointer `/components/schemas/Foo`.
131- fn local_ref_to_pointer ( ref_str : & str ) -> Option < String > {
132- ref_str. strip_prefix ( '#' ) . map ( |s| s. to_string ( ) )
133- }
134-
135110/// Walk `schema` and inject `"additionalProperties": false` on every
136111/// `type: object` node (or any object node without an explicit type) that
137112/// doesn't already have `additionalProperties` set. Recurses into
@@ -355,54 +330,40 @@ mod tests {
355330 }
356331
357332 #[ test]
358- fn inline_refs_simple ( ) {
359- let root = json ! ( {
360- "components" : {
361- "schemas" : {
362- "Address" : { "type" : "object" , "properties" : { "city" : { "type" : "string" } } }
363- }
364- }
365- } ) ;
333+ fn rewrite_refs_rewrites_local_refs ( ) {
366334 let schema = json ! ( {
367335 "type" : "object" ,
368336 "properties" : {
369337 "addr" : { "$ref" : "#/components/schemas/Address" }
370338 }
371339 } ) ;
372- let result = inline_refs ( schema, & root, & mut HashSet :: new ( ) ) ;
373- let addr = & result[ "properties" ] [ "addr" ] ;
374- assert_eq ! ( addr[ "type" ] , "object" ) ;
375- assert ! ( addr. get( "$ref" ) . is_none( ) ) ;
340+ let result = rewrite_refs ( schema) ;
341+ assert_eq ! (
342+ result[ "properties" ] [ "addr" ] [ "$ref" ] ,
343+ format!( "{SYNTHETIC_OPENAPI_ROOT_URI}#/components/schemas/Address" )
344+ ) ;
376345 }
377346
378347 #[ test]
379- fn inline_refs_cycle ( ) {
380- // LinkedList node references itself.
381- let root = json ! ( {
382- "components" : {
383- "schemas" : {
384- "Node" : {
385- "type" : "object" ,
386- "properties" : {
387- "next" : { "$ref" : "#/components/schemas/Node" }
388- }
389- }
390- }
348+ fn rewrite_refs_leaves_external_refs_alone ( ) {
349+ let schema = json ! ( { "$ref" : "https://example.com/schema.json" } ) ;
350+ let result = rewrite_refs ( schema) ;
351+ assert_eq ! ( result[ "$ref" ] , "https://example.com/schema.json" ) ;
352+ }
353+
354+ #[ test]
355+ fn rewrite_refs_cycle_safe ( ) {
356+ // Deep nesting should not stack-overflow — rewrite_refs never recurses
357+ // into the value of a $ref, it just rewrites the string.
358+ let schema = json ! ( {
359+ "type" : "object" ,
360+ "properties" : {
361+ "next" : { "$ref" : "#/components/schemas/Node" }
391362 }
392363 } ) ;
393- let schema = root[ "components" ] [ "schemas" ] [ "Node" ] . clone ( ) ;
394- // Should not stack-overflow. The first-level ref is expanded once;
395- // the nested forward ref is preserved as a $ref string so the
396- // jsonschema compiler can resolve it against the root doc.
397- let result = inline_refs ( schema, & root, & mut HashSet :: new ( ) ) ;
398- // First expansion: next becomes the Node object.
399- assert_eq ! ( result[ "properties" ] [ "next" ] [ "type" ] , "object" ) ;
400- // Second level: the nested next ref is preserved (cycle guard fired)
401- // but rewritten to the synthetic root URI so the jsonschema compiler
402- // can resolve it via the registered root document.
403- let nested_next = & result[ "properties" ] [ "next" ] [ "properties" ] [ "next" ] ;
364+ let result = rewrite_refs ( schema) ;
404365 assert_eq ! (
405- nested_next [ "$ref" ] ,
366+ result [ "properties" ] [ "next" ] [ "$ref" ] ,
406367 format!( "{SYNTHETIC_OPENAPI_ROOT_URI}#/components/schemas/Node" )
407368 ) ;
408369 }
0 commit comments