@@ -178,33 +178,61 @@ fn resolve_external_ref(
178178 }
179179 } ;
180180
181- // Generate a unique name
181+ // Generate a unique name and reserve it. Insert into `rewrites`
182+ // before recursing so circular refs see the in-progress entry and
183+ // terminate instead of looping.
182184 let base_name = generate_external_name ( & doc_uri, & fragment) ;
183185 let name = make_unique_name ( & base_name, used_names) ;
184186 used_names. insert ( name. clone ( ) ) ;
185-
186- // Register as a definition
187- if let Ok ( parsed) = serde_json:: from_value :: < schemars:: schema:: Schema > ( sub_schema. clone ( ) ) {
188- definitions. insert ( name. clone ( ) , parsed) ;
189- }
190-
191187 rewrites. insert ( ref_str. to_string ( ) , format ! ( "#/definitions/{}" , name) ) ;
192188
193- // Recursively resolve refs within the external schema
189+ // Walk every ref inside the copied sub-schema. For each:
190+ // - external ref (no leading `#`) — resolve as before.
191+ // - internal ref (`#/definitions/X`) — refers to a sibling
192+ // definition in the same external document. The bundler must
193+ // pull that sibling into the root's definitions and rewrite the
194+ // internal ref to the new bundled name, otherwise the copied
195+ // sub-schema points into a namespace that no longer exists.
196+ //
197+ // We treat each internal ref as a synthetic external ref
198+ // `{doc_uri}{#fragment}` and recurse — that produces a single,
199+ // shared bundled definition no matter how many places mention it.
194200 let nested_refs = collect_refs ( & sub_schema) ;
201+ let mut local_rewrites: BTreeMap < String , String > = BTreeMap :: new ( ) ;
195202 for nested_ref in nested_refs {
196- if !nested_ref. starts_with ( '#' ) {
197- // Resolve relative to the same document's directory
198- resolve_external_ref (
199- & nested_ref,
200- external_schemas,
201- definitions,
202- rewrites,
203- used_names,
204- visited,
205- ) ;
203+ let synthetic_target = if let Some ( internal) = nested_ref. strip_prefix ( '#' ) {
204+ format ! ( "{}#{}" , doc_uri, internal)
205+ } else {
206+ nested_ref. clone ( )
207+ } ;
208+
209+ resolve_external_ref (
210+ & synthetic_target,
211+ external_schemas,
212+ definitions,
213+ rewrites,
214+ used_names,
215+ visited,
216+ ) ;
217+
218+ if nested_ref. starts_with ( '#' ) {
219+ if let Some ( new_target) = rewrites. get ( & synthetic_target) {
220+ local_rewrites. insert ( nested_ref, new_target. clone ( ) ) ;
221+ }
206222 }
207223 }
224+
225+ // Apply the locally-collected rewrites to this sub-schema before
226+ // registering it, so internal refs in the copied definition resolve
227+ // to their bundled siblings.
228+ let mut bundled_schema = sub_schema;
229+ if !local_rewrites. is_empty ( ) {
230+ rewrite_refs ( & mut bundled_schema, & local_rewrites) ;
231+ }
232+
233+ if let Ok ( parsed) = serde_json:: from_value :: < schemars:: schema:: Schema > ( bundled_schema) {
234+ definitions. insert ( name, parsed) ;
235+ }
208236}
209237
210238/// Recursively collect all `$ref` string values from a JSON value.
@@ -459,6 +487,77 @@ mod tests {
459487 }
460488 }
461489
490+ #[ test]
491+ fn test_bundle_external_with_internal_sibling_ref ( ) {
492+ // External `types.json` has `Outer` that references its sibling
493+ // `Inner` via an internal `#/definitions/Inner` ref. After
494+ // bundling, both must be present in the root definitions and
495+ // `Outer`'s ref must be rewritten to point at the bundled
496+ // `Inner` (not the original namespace, which no longer exists).
497+ let mut schema = root_schema_from_json ( json ! ( {
498+ "type" : "object" ,
499+ "properties" : {
500+ "outer" : { "$ref" : "types.json#/definitions/Outer" }
501+ }
502+ } ) ) ;
503+
504+ let mut externals = BTreeMap :: new ( ) ;
505+ externals. insert (
506+ "types.json" . to_string ( ) ,
507+ json ! ( {
508+ "definitions" : {
509+ "Outer" : {
510+ "type" : "object" ,
511+ "properties" : {
512+ "inner" : { "$ref" : "#/definitions/Inner" }
513+ }
514+ } ,
515+ "Inner" : { "type" : "string" }
516+ }
517+ } ) ,
518+ ) ;
519+
520+ bundle_external_refs ( & mut schema, & externals) ;
521+
522+ // The root `outer` ref points at the bundled Outer.
523+ let outer = & schema. schema . object . as_ref ( ) . unwrap ( ) . properties [ "outer" ] ;
524+ let outer_ref = if let schemars:: schema:: Schema :: Object ( obj) = outer {
525+ obj. reference . clone ( ) . unwrap ( )
526+ } else {
527+ panic ! ( "expected schema object" )
528+ } ;
529+ let outer_name = outer_ref. strip_prefix ( "#/definitions/" ) . unwrap ( ) ;
530+
531+ // Both Outer and Inner are bundled.
532+ let outer_def = schema. definitions . get ( outer_name) . unwrap ( ) ;
533+ let inner_ref = if let schemars:: schema:: Schema :: Object ( obj) = outer_def {
534+ obj. object
535+ . as_ref ( )
536+ . unwrap ( )
537+ . properties
538+ . get ( "inner" )
539+ . and_then ( |s| {
540+ if let schemars:: schema:: Schema :: Object ( o) = s {
541+ o. reference . clone ( )
542+ } else {
543+ None
544+ }
545+ } )
546+ . unwrap ( )
547+ } else {
548+ panic ! ( "expected schema object" )
549+ } ;
550+
551+ // Outer's nested ref is rewritten to the bundled Inner — not
552+ // the original `#/definitions/Inner` that no longer exists.
553+ let inner_name = inner_ref. strip_prefix ( "#/definitions/" ) . unwrap ( ) ;
554+ assert ! (
555+ schema. definitions. contains_key( inner_name) ,
556+ "rewritten inner ref {} not found in definitions" ,
557+ inner_ref
558+ ) ;
559+ }
560+
462561 #[ test]
463562 fn test_generate_name_from_pointer ( ) {
464563 let used = BTreeSet :: new ( ) ;
0 commit comments