@@ -22,171 +22,170 @@ module.exports = bundle;
2222 * @param {$RefParserOptions } options
2323 */
2424function bundle ( parser , options ) {
25- util . debug ( 'Bundling $ref pointers in %s' , parser . _basePath ) ;
25+ util . debug ( 'Bundling $ref pointers in %s' , parser . $refs . _basePath ) ;
2626
27- optimize ( parser . $refs ) ;
28- remap ( parser . $refs , options ) ;
29- dereference ( parser . _basePath , parser . $refs , options ) ;
30- }
27+ // Build an inventory of all $ref pointers in the JSON Schema
28+ var inventory = [ ] ;
29+ crawl ( parser . schema , parser . $refs . _basePath + '#' , '#' , inventory , parser . $refs , options ) ;
3130
32- /**
33- * Optimizes the {@link $Ref#referencedAt} list for each {@link $Ref} to contain as few entries
34- * as possible (ideally, one).
35- *
36- * @example :
37- * {
38- * first: { $ref: somefile.json#/some/part },
39- * second: { $ref: somefile.json#/another/part },
40- * third: { $ref: somefile.json },
41- * fourth: { $ref: somefile.json#/some/part/sub/part }
42- * }
43- *
44- * In this example, there are four references to the same file, but since the third reference points
45- * to the ENTIRE file, that's the only one we care about. The other three can just be remapped to point
46- * inside the third one.
47- *
48- * On the other hand, if the third reference DIDN'T exist, then the first and second would both be
49- * significant, since they point to different parts of the file. The fourth reference is not significant,
50- * since it can still be remapped to point inside the first one.
51- *
52- * @param {$Refs } $refs
53- */
54- function optimize ( $refs ) {
55- Object . keys ( $refs . _$refs ) . forEach ( function ( key ) {
56- var $ref = $refs . _$refs [ key ] ;
57-
58- // Find the first reference to this $ref
59- var first = $ref . referencedAt . filter ( function ( at ) { return at . firstReference ; } ) [ 0 ] ;
60-
61- // Do any of the references point to the entire file?
62- var entireFile = $ref . referencedAt . filter ( function ( at ) { return at . hash === '#' ; } ) ;
63- if ( entireFile . length === 1 ) {
64- // We found a single reference to the entire file. Done!
65- $ref . referencedAt = entireFile ;
66- }
67- else if ( entireFile . length > 1 ) {
68- // We found more than one reference to the entire file. Pick the first one.
69- if ( entireFile . indexOf ( first ) >= 0 ) {
70- $ref . referencedAt = [ first ] ;
71- }
72- else {
73- $ref . referencedAt = entireFile . slice ( 0 , 1 ) ;
74- }
75- }
76- else {
77- // There are noo references to the entire file, so optimize the list of reference points
78- // by eliminating any duplicate/redundant ones (e.g. "fourth" in the example above)
79- console . log ( '========================= %s BEFORE =======================' , $ref . path , JSON . stringify ( $ref . referencedAt , null , 2 ) ) ;
80- [ first ] . concat ( $ref . referencedAt ) . forEach ( function ( at ) {
81- dedupe ( at , $ref . referencedAt ) ;
82- } ) ;
83- console . log ( '========================= %s AFTER =======================' , $ref . path , JSON . stringify ( $ref . referencedAt , null , 2 ) ) ;
84- }
85- } ) ;
86- }
87-
88- /**
89- * Removes redundant entries from the {@link $Ref#referencedAt} list.
90- *
91- * @param {object } original - The {@link $Ref#referencedAt} entry to keep
92- * @param {object[] } dupes - The {@link $Ref#referencedAt} list to dedupe
93- */
94- function dedupe ( original , dupes ) {
95- for ( var i = dupes . length - 1 ; i >= 0 ; i -- ) {
96- var dupe = dupes [ i ] ;
97- if ( dupe !== original && dupe . hash . indexOf ( original . hash ) === 0 ) {
98- dupes . splice ( i , 1 ) ;
99- }
100- }
31+ // Remap all $ref pointers
32+ remap ( inventory ) ;
10133}
10234
10335/**
104- * Re-maps all $ref pointers in the schema, so that they are relative to the root of the schema .
36+ * Recursively crawls the given value, and inventories all JSON references .
10537 *
38+ * @param {* } obj - The value to crawl. If it's not an object or array, it will be ignored.
39+ * @param {string } path - The full path of `obj`, possibly with a JSON Pointer in the hash
40+ * @param {string } pathFromRoot - The path of `obj` from the schema root
41+ * @param {object[] } inventory - An array of already-inventoried $ref pointers
10642 * @param {$Refs } $refs
10743 * @param {$RefParserOptions } options
10844 */
109- function remap ( $refs , options ) {
110- var remapped = [ ] ;
111-
112- // Crawl the schema and determine the re-mapped values for all $ref pointers.
113- // NOTE: We don't actually APPLY the re-mappings yet, since that can affect other re-mappings
114- Object . keys ( $refs . _$refs ) . forEach ( function ( key ) {
115- var $ref = $refs . _$refs [ key ] ;
116- crawl ( $ref . value , $ref . path + '#' , $refs , remapped , options ) ;
117- } ) ;
45+ function crawl ( obj , path , pathFromRoot , inventory , $refs , options ) {
46+ if ( obj && typeof obj === 'object' ) {
47+ var keys = Object . keys ( obj ) ;
11848
119- // Now APPLY all of the re-mappings
120- for ( var i = 0 ; i < remapped . length ; i ++ ) {
121- var mapping = remapped [ i ] ;
122- mapping . old$Ref . $ref = mapping . new$Ref . $ref ;
123- }
124- }
49+ // Most people will expect references to be bundled into the the "definitions" property,
50+ // so we always crawl that property first, if it exists.
51+ var defs = keys . indexOf ( 'definitions' ) ;
52+ if ( defs > 0 ) {
53+ keys . splice ( 0 , 0 , keys . splice ( defs , 1 ) [ 0 ] ) ;
54+ }
12555
126- /**
127- * Recursively crawls the given value, and re-maps any JSON references.
128- *
129- * @param {* } obj - The value to crawl. If it's not an object or array, it will be ignored.
130- * @param {string } path - The path to use for resolving relative JSON references
131- * @param {$Refs } $refs - The resolved JSON references
132- * @param {object[] } remapped - An array of the re-mapped JSON references
133- * @param {$RefParserOptions } options
134- */
135- function crawl ( obj , path , $refs , remapped , options ) {
136- if ( obj && typeof obj === 'object' ) {
137- Object . keys ( obj ) . forEach ( function ( key ) {
56+ keys . forEach ( function ( key ) {
13857 var keyPath = Pointer . join ( path , key ) ;
58+ var keyPathFromRoot = Pointer . join ( pathFromRoot , key ) ;
13959 var value = obj [ key ] ;
14060
14161 if ( $Ref . is$Ref ( value ) ) {
142- // We found a $ref, so resolve it
143- util . debug ( 'Re-mapping $ref pointer "%s" at %s' , value . $ref , keyPath ) ;
144- var $refPath = url . resolve ( path , value . $ref ) ;
145- var pointer = $refs . _resolve ( $refPath , options ) ;
146-
147- // Find the path from the root of the JSON schema
148- var hash = util . path . getHash ( value . $ref ) ;
149- var referencedAt = pointer . $ref . referencedAt . filter ( function ( at ) {
150- return hash . indexOf ( at . hash ) === 0 ;
151- } ) [ 0 ] ;
152-
153- console . log (
154- 'referencedAt.pathFromRoot =' , referencedAt . pathFromRoot ,
155- '\nreferencedAt.hash =' , referencedAt . hash ,
156- '\nhash =' , hash ,
157- '\npointer.path.hash =' , util . path . getHash ( pointer . path )
158- ) ;
159-
160- // Re-map the value
161- var new$RefPath = referencedAt . pathFromRoot + util . path . getHash ( pointer . path ) . substr ( 1 ) ;
162- util . debug ( ' new value: %s' , new$RefPath ) ;
163- remapped . push ( {
164- old$Ref : value ,
165- new$Ref : { $ref : new$RefPath } // Note: DON'T name this property `new` (https://github.com/BigstickCarpet/json-schema-ref-parser/issues/3)
166- } ) ;
62+ // Skip this $ref if we've already inventoried it
63+ if ( ! inventory . some ( function ( i ) { return i . parent === obj && i . key === key ; } ) ) {
64+ inventory$Ref ( obj , key , path , keyPathFromRoot , inventory , $refs , options ) ;
65+ }
16766 }
16867 else {
169- crawl ( value , keyPath , $refs , remapped , options ) ;
68+ crawl ( value , keyPath , keyPathFromRoot , inventory , $refs , options ) ;
17069 }
17170 } ) ;
17271 }
17372}
17473
17574/**
176- * Dereferences each external $ref pointer exactly ONCE.
75+ * Inventories the given JSON Reference (i.e. records detailed information about it so we can
76+ * optimize all $refs in the schema), and then crawls the resolved value.
17777 *
178- * @param {string } basePath
78+ * @param {object } $refParent - The object that contains a JSON Reference as one of its keys
79+ * @param {string } $refKey - The key in `$refParent` that is a JSON Reference
80+ * @param {string } path - The full path of the JSON Reference at `$refKey`, possibly with a JSON Pointer in the hash
81+ * @param {string } pathFromRoot - The path of the JSON Reference at `$refKey`, from the schema root
82+ * @param {object[] } inventory - An array of already-inventoried $ref pointers
17983 * @param {$Refs } $refs
18084 * @param {$RefParserOptions } options
18185 */
182- function dereference ( basePath , $refs , options ) {
183- basePath = util . path . stripHash ( basePath ) ;
86+ function inventory$Ref ( $refParent , $refKey , path , pathFromRoot , inventory , $refs , options ) {
87+ var $ref = $refParent [ $refKey ] ;
88+ var $refPath = url . resolve ( path , $ref . $ref ) ;
89+ var pointer = $refs . _resolve ( $refPath , options ) ;
90+ var depth = Pointer . parse ( pathFromRoot ) . length ;
91+ var file = util . path . stripHash ( pointer . path ) ;
92+ var hash = util . path . getHash ( pointer . path ) ;
93+ var external = file !== $refs . _basePath ;
94+ var extended = Object . keys ( $ref ) . length > 1 ;
95+
96+ inventory . push ( {
97+ $ref : $ref , // The JSON Reference (e.g. {$ref: string})
98+ parent : $refParent , // The object that contains this $ref pointer
99+ key : $refKey , // The key in `parent` that is the $ref pointer
100+ pathFromRoot : pathFromRoot , // The path to the $ref pointer, from the JSON Schema root
101+ depth : depth , // How far from the JSON Schema root is this $ref pointer?
102+ file : file , // The file that the $ref pointer resolves to
103+ hash : hash , // The hash within `file` that the $ref pointer resolves to
104+ value : pointer . value , // The resolved value of the $ref pointer
105+ circular : pointer . circular , // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)
106+ extended : extended , // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to "$ref")
107+ external : external // Does this $ref pointer point to a file other than the main JSON Schema file?
108+ } ) ;
184109
185- Object . keys ( $refs . _$refs ) . forEach ( function ( key ) {
186- var $ref = $refs . _$refs [ key ] ;
110+ // Recursively crawl the resolved value
111+ crawl ( pointer . value , pointer . path , pathFromRoot , inventory , $refs , options ) ;
112+ }
187113
188- if ( $ref . referencedAt . length > 0 ) {
189- $refs . set ( basePath + $ref . referencedAt [ 0 ] . pathFromRoot , $ref . value , options ) ;
114+ /**
115+ * Re-maps every $ref pointer, so that they're all relative to the root of the JSON Schema.
116+ * Each referenced value is dereferenced EXACTLY ONCE. All subsequent references to the same
117+ * value are re-mapped to point to the first reference.
118+ *
119+ * @example :
120+ * {
121+ * first: { $ref: somefile.json#/some/part },
122+ * second: { $ref: somefile.json#/another/part },
123+ * third: { $ref: somefile.json },
124+ * fourth: { $ref: somefile.json#/some/part/sub/part }
125+ * }
126+ *
127+ * In this example, there are four references to the same file, but since the third reference points
128+ * to the ENTIRE file, that's the only one we need to dereference. The other three can just be
129+ * remapped to point inside the third one.
130+ *
131+ * On the other hand, if the third reference DIDN'T exist, then the first and second would both need
132+ * to be dereferenced, since they point to different parts of the file. The fourth reference does NOT
133+ * need to be dereferenced, because it can be remapped to point inside the first one.
134+ *
135+ * @param {object[] } inventory
136+ */
137+ function remap ( inventory ) {
138+ // Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
139+ inventory . sort ( function ( a , b ) {
140+ if ( a . file !== b . file ) {
141+ return a . file < b . file ? - 1 : + 1 ; // Group all the $refs that point to the same file
142+ }
143+ else if ( a . hash !== b . hash ) {
144+ return a . hash < b . hash ? - 1 : + 1 ; // Group all the $refs that point to the same part of the file
145+ }
146+ else if ( a . circular !== b . circular ) {
147+ return a . circular ? - 1 : + 1 ; // If the $ref points to itself, then sort it higher than other $refs that point to this $ref
148+ }
149+ else if ( a . extended !== b . extended ) {
150+ return a . extended ? + 1 : - 1 ; // If the $ref extends the resolved value, then sort it lower than other $refs that don't extend the value
151+ }
152+ else if ( a . depth !== b . depth ) {
153+ return a . depth - b . depth ; // Sort $refs by how close they are to the JSON Schema root
154+ }
155+ else {
156+ // If all else is equal, use the $ref that's in the "definitions" property
157+ return b . pathFromRoot . lastIndexOf ( '/definitions' ) - a . pathFromRoot . lastIndexOf ( '/definitions' ) ;
190158 }
191159 } ) ;
160+
161+ var file , hash , pathFromRoot ;
162+ inventory . forEach ( function ( i ) {
163+ util . debug ( 'Re-mapping $ref pointer "%s" at %s' , i . $ref . $ref , i . pathFromRoot ) ;
164+
165+ if ( ! i . external ) {
166+ // This $ref already resolves to the main JSON Schema file
167+ i . $ref . $ref = i . hash ;
168+ }
169+ else if ( i . file !== file || i . hash . indexOf ( hash ) !== 0 ) {
170+ // We've moved to a new file or new hash
171+ file = i . file ;
172+ hash = i . hash ;
173+ pathFromRoot = i . pathFromRoot ;
174+
175+ // This is the first $ref to point to this value, so dereference the value.
176+ // Any other $refs that point to the same value will point to this $ref instead
177+ i . $ref = i . parent [ i . key ] = util . dereference ( i . $ref , i . value ) ;
178+
179+ if ( i . circular ) {
180+ // This $ref points to itself
181+ i . $ref . $ref = i . pathFromRoot ;
182+ }
183+ }
184+ else {
185+ // This $ref points to the same value as the prevous $ref
186+ i . $ref . $ref = Pointer . join ( pathFromRoot , Pointer . parse ( i . hash ) ) ;
187+ }
188+
189+ util . debug ( ' new value: %s' , ( i . $ref && i . $ref . $ref ) ? i . $ref . $ref : '[object Object]' ) ;
190+ } ) ;
192191}
0 commit comments