fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

test: add unit tests for sibling schema resolution during bundling

Cover the fallback resolution and crawl path rebase fixes in
inventory$Ref with five scenarios: bare wrapper chain, extended
wrapper chain, direct reference (no wrapper), multiple siblings
through an extended wrapper, and collision handling when two
external files expose same-named sibling schemas.

Signed-off-by: Jason Westover <jwestover@nvidia.com>

+482
+227
packages/json-schema-ref-parser/src/__tests__/bundle.test.ts
··· 81 81 82 82 await expectBundledSchemaToMatchSnapshot(schema, 'redfish-like.json'); 83 83 }); 84 + 85 + describe('sibling schema resolution', () => { 86 + const specsDir = path.join(getSpecsPath(), 'json-schema-ref-parser'); 87 + 88 + const findSchemaByValue = ( 89 + schemas: Record<string, any>, 90 + predicate: (value: any) => boolean, 91 + ): [string, any] | undefined => { 92 + for (const [name, value] of Object.entries(schemas)) { 93 + if (predicate(value)) { 94 + return [name, value]; 95 + } 96 + } 97 + return undefined; 98 + }; 99 + 100 + it('hoists sibling schemas through a bare $ref wrapper chain', async () => { 101 + const refParser = new $RefParser(); 102 + const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-root.json'); 103 + const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 104 + 105 + expect(schema.components).toBeDefined(); 106 + expect(schema.components.schemas).toBeDefined(); 107 + 108 + const schemas = schema.components.schemas; 109 + 110 + const mainSchema = findSchemaByValue( 111 + schemas, 112 + (v) => v.type === 'object' && v.properties?.name, 113 + ); 114 + expect(mainSchema).toBeDefined(); 115 + const [mainName, mainValue] = mainSchema!; 116 + expect(mainValue.type).toBe('object'); 117 + expect(mainValue.properties.name).toEqual({ type: 'string' }); 118 + 119 + const enumSchema = findSchemaByValue( 120 + schemas, 121 + (v) => Array.isArray(v.enum) && v.enum.includes('active'), 122 + ); 123 + expect(enumSchema).toBeDefined(); 124 + const [enumName, enumValue] = enumSchema!; 125 + expect(enumValue.type).toBe('string'); 126 + expect(enumValue.enum).toEqual(['active', 'inactive', 'pending']); 127 + 128 + // The main schema's status property should reference the hoisted enum 129 + expect(mainValue.properties.status.$ref).toBe(`#/components/schemas/${enumName}`); 130 + 131 + // The root path's schema ref should point to the hoisted main schema 132 + const rootRef = schema.paths['/test'].get.responses['200'].content['application/json'].schema; 133 + expect(rootRef.$ref).toBe(`#/components/schemas/${mainName}`); 134 + }); 135 + 136 + it('hoists sibling schemas through an extended $ref wrapper chain', async () => { 137 + const refParser = new $RefParser(); 138 + const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-extended-root.json'); 139 + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); 140 + 141 + try { 142 + const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 143 + 144 + expect(schema.components).toBeDefined(); 145 + expect(schema.components.schemas).toBeDefined(); 146 + 147 + const schemas = schema.components.schemas; 148 + 149 + // The main schema should be hoisted (with the extra description merged in) 150 + const mainSchema = findSchemaByValue( 151 + schemas, 152 + (v) => 153 + v.description === 'Wrapper that extends the versioned schema' || 154 + (v.type === 'object' && v.properties?.name), 155 + ); 156 + expect(mainSchema).toBeDefined(); 157 + 158 + // The sibling enum must also be hoisted (this was the bug — it was lost before the fix) 159 + const enumSchema = findSchemaByValue( 160 + schemas, 161 + (v) => Array.isArray(v.enum) && v.enum.includes('active'), 162 + ); 163 + expect(enumSchema).toBeDefined(); 164 + const [, enumValue] = enumSchema!; 165 + expect(enumValue.type).toBe('string'); 166 + expect(enumValue.enum).toEqual(['active', 'inactive', 'pending']); 167 + 168 + // No "Skipping unresolvable $ref" warnings should have been emitted 169 + const unresolvableWarnings = warnSpy.mock.calls.filter( 170 + (args) => typeof args[0] === 'string' && args[0].includes('Skipping unresolvable $ref'), 171 + ); 172 + expect(unresolvableWarnings).toHaveLength(0); 173 + } finally { 174 + warnSpy.mockRestore(); 175 + } 176 + }); 177 + 178 + it('hoists sibling schemas from a direct reference (no wrapper)', async () => { 179 + const refParser = new $RefParser(); 180 + const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-direct-root.json'); 181 + const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 182 + 183 + expect(schema.components).toBeDefined(); 184 + expect(schema.components.schemas).toBeDefined(); 185 + 186 + const schemas = schema.components.schemas; 187 + 188 + const mainSchema = findSchemaByValue( 189 + schemas, 190 + (v) => v.type === 'object' && v.properties?.name, 191 + ); 192 + expect(mainSchema).toBeDefined(); 193 + 194 + const enumSchema = findSchemaByValue( 195 + schemas, 196 + (v) => Array.isArray(v.enum) && v.enum.includes('active'), 197 + ); 198 + expect(enumSchema).toBeDefined(); 199 + const [enumName, enumValue] = enumSchema!; 200 + expect(enumValue.enum).toEqual(['active', 'inactive', 'pending']); 201 + 202 + const [, mainValue] = mainSchema!; 203 + expect(mainValue.properties.status.$ref).toBe(`#/components/schemas/${enumName}`); 204 + }); 205 + 206 + it('hoists multiple sibling schemas through an extended wrapper', async () => { 207 + const refParser = new $RefParser(); 208 + const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-multi-root.json'); 209 + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); 210 + 211 + try { 212 + const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 213 + 214 + expect(schema.components).toBeDefined(); 215 + expect(schema.components.schemas).toBeDefined(); 216 + 217 + const schemas = schema.components.schemas; 218 + 219 + const mainSchema = findSchemaByValue( 220 + schemas, 221 + (v) => v.type === 'object' && v.properties?.health, 222 + ); 223 + expect(mainSchema).toBeDefined(); 224 + 225 + const statusEnum = findSchemaByValue( 226 + schemas, 227 + (v) => Array.isArray(v.enum) && v.enum.includes('enabled'), 228 + ); 229 + expect(statusEnum).toBeDefined(); 230 + expect(statusEnum![1].enum).toEqual(['enabled', 'disabled', 'standby']); 231 + 232 + const healthEnum = findSchemaByValue( 233 + schemas, 234 + (v) => Array.isArray(v.enum) && v.enum.includes('ok'), 235 + ); 236 + expect(healthEnum).toBeDefined(); 237 + expect(healthEnum![1].enum).toEqual(['ok', 'warning', 'critical']); 238 + 239 + const [, mainValue] = mainSchema!; 240 + expect(mainValue.properties.status.$ref).toBe(`#/components/schemas/${statusEnum![0]}`); 241 + expect(mainValue.properties.health.$ref).toBe(`#/components/schemas/${healthEnum![0]}`); 242 + 243 + const unresolvableWarnings = warnSpy.mock.calls.filter( 244 + (args) => typeof args[0] === 'string' && args[0].includes('Skipping unresolvable $ref'), 245 + ); 246 + expect(unresolvableWarnings).toHaveLength(0); 247 + } finally { 248 + warnSpy.mockRestore(); 249 + } 250 + }); 251 + 252 + it('handles multiple external files with same-named sibling schemas', async () => { 253 + const refParser = new $RefParser(); 254 + const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-collision-root.json'); 255 + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); 256 + 257 + try { 258 + const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 259 + 260 + expect(schema.components).toBeDefined(); 261 + expect(schema.components.schemas).toBeDefined(); 262 + 263 + const schemas = schema.components.schemas; 264 + const schemaNames = Object.keys(schemas); 265 + 266 + const mainSchemaKey = schemaNames.find((name) => name.includes('MainSchema')); 267 + const otherSchemaKey = schemaNames.find((name) => name.includes('OtherSchema')); 268 + 269 + expect(mainSchemaKey).toBeDefined(); 270 + expect(otherSchemaKey).toBeDefined(); 271 + 272 + const statusSchemas = schemaNames.filter((name) => name.includes('Status')); 273 + expect(statusSchemas.length).toBeGreaterThanOrEqual(2); 274 + 275 + const statusValues = statusSchemas.map((name) => schemas[name]); 276 + const stringStatus = statusValues.find((v: any) => v.type === 'string'); 277 + const integerStatus = statusValues.find((v: any) => v.type === 'integer'); 278 + 279 + expect(stringStatus).toBeDefined(); 280 + expect(integerStatus).toBeDefined(); 281 + expect(stringStatus!.enum).toEqual(['active', 'inactive']); 282 + expect(integerStatus!.enum).toEqual([0, 1, 2]); 283 + 284 + const mainSchemaValue = schemas[mainSchemaKey!]; 285 + const mainStatusRef = mainSchemaValue.properties.status.$ref; 286 + expect(mainStatusRef).toMatch(/^#\/components\/schemas\/.*Status/); 287 + 288 + const referencedStatus = schemas[mainStatusRef.replace('#/components/schemas/', '')]; 289 + expect(referencedStatus).toBeDefined(); 290 + expect(referencedStatus.type).toBe('string'); 291 + expect(referencedStatus.enum).toEqual(['active', 'inactive']); 292 + 293 + const otherSchemaValue = schemas[otherSchemaKey!]; 294 + const otherStatusRef = otherSchemaValue.properties.code.$ref; 295 + expect(otherStatusRef).toMatch(/^#\/components\/schemas\/.*Status/); 296 + 297 + const referencedOtherStatus = schemas[otherStatusRef.replace('#/components/schemas/', '')]; 298 + expect(referencedOtherStatus).toBeDefined(); 299 + expect(referencedOtherStatus.type).toBe('integer'); 300 + expect(referencedOtherStatus.enum).toEqual([0, 1, 2]); 301 + 302 + const unresolvableWarnings = warnSpy.mock.calls.filter( 303 + (args) => typeof args[0] === 'string' && args[0].includes('Skipping unresolvable $ref'), 304 + ); 305 + expect(unresolvableWarnings).toHaveLength(0); 306 + } finally { 307 + warnSpy.mockRestore(); 308 + } 309 + }); 310 + }); 84 311 });
+18
specs/json-schema-ref-parser/sibling-schema-collision-other.json
··· 1 + { 2 + "components": { 3 + "schemas": { 4 + "OtherSchema": { 5 + "type": "object", 6 + "properties": { 7 + "code": { 8 + "$ref": "#/components/schemas/Status" 9 + } 10 + } 11 + }, 12 + "Status": { 13 + "type": "integer", 14 + "enum": [0, 1, 2] 15 + } 16 + } 17 + } 18 + }
+38
specs/json-schema-ref-parser/sibling-schema-collision-root.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { "title": "Collision Test", "version": "1.0.0" }, 4 + "paths": { 5 + "/main": { 6 + "get": { 7 + "responses": { 8 + "200": { 9 + "description": "ok", 10 + "content": { 11 + "application/json": { 12 + "schema": { 13 + "$ref": "sibling-schema-collision-wrapper.json#/components/schemas/MainSchema" 14 + } 15 + } 16 + } 17 + } 18 + } 19 + } 20 + }, 21 + "/other": { 22 + "get": { 23 + "responses": { 24 + "200": { 25 + "description": "ok", 26 + "content": { 27 + "application/json": { 28 + "schema": { 29 + "$ref": "sibling-schema-collision-other.json#/components/schemas/OtherSchema" 30 + } 31 + } 32 + } 33 + } 34 + } 35 + } 36 + } 37 + } 38 + }
+18
specs/json-schema-ref-parser/sibling-schema-collision-versioned.json
··· 1 + { 2 + "components": { 3 + "schemas": { 4 + "MainSchema": { 5 + "type": "object", 6 + "properties": { 7 + "status": { 8 + "$ref": "#/components/schemas/Status" 9 + } 10 + } 11 + }, 12 + "Status": { 13 + "type": "string", 14 + "enum": ["active", "inactive"] 15 + } 16 + } 17 + } 18 + }
+9
specs/json-schema-ref-parser/sibling-schema-collision-wrapper.json
··· 1 + { 2 + "components": { 3 + "schemas": { 4 + "MainSchema": { 5 + "$ref": "sibling-schema-collision-versioned.json#/components/schemas/MainSchema" 6 + } 7 + } 8 + } 9 + }
+22
specs/json-schema-ref-parser/sibling-schema-direct-root.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { "title": "Direct Sibling Schema Test", "version": "1.0.0" }, 4 + "paths": { 5 + "/test": { 6 + "get": { 7 + "responses": { 8 + "200": { 9 + "description": "ok", 10 + "content": { 11 + "application/json": { 12 + "schema": { 13 + "$ref": "sibling-schema-versioned.json#/components/schemas/Versioned_Schema" 14 + } 15 + } 16 + } 17 + } 18 + } 19 + } 20 + } 21 + } 22 + }
+25
specs/json-schema-ref-parser/sibling-schema-extended-root.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { 4 + "title": "Extended Wrapper Sibling Schema Test", 5 + "version": "1.0.0" 6 + }, 7 + "paths": { 8 + "/test": { 9 + "get": { 10 + "responses": { 11 + "200": { 12 + "description": "ok", 13 + "content": { 14 + "application/json": { 15 + "schema": { 16 + "$ref": "sibling-schema-extended-wrapper.json#/components/schemas/Wrapper_Schema" 17 + } 18 + } 19 + } 20 + } 21 + } 22 + } 23 + } 24 + } 25 + }
+10
specs/json-schema-ref-parser/sibling-schema-extended-wrapper.json
··· 1 + { 2 + "components": { 3 + "schemas": { 4 + "Wrapper_Schema": { 5 + "description": "Wrapper that extends the versioned schema", 6 + "$ref": "sibling-schema-versioned.json#/components/schemas/Versioned_Schema" 7 + } 8 + } 9 + } 10 + }
+25
specs/json-schema-ref-parser/sibling-schema-multi-root.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { 4 + "title": "Multiple Siblings Test", 5 + "version": "1.0.0" 6 + }, 7 + "paths": { 8 + "/resource": { 9 + "get": { 10 + "responses": { 11 + "200": { 12 + "description": "ok", 13 + "content": { 14 + "application/json": { 15 + "schema": { 16 + "$ref": "sibling-schema-multi-wrapper.json#/components/schemas/Resource_Schema" 17 + } 18 + } 19 + } 20 + } 21 + } 22 + } 23 + } 24 + } 25 + }
+28
specs/json-schema-ref-parser/sibling-schema-multi-versioned.json
··· 1 + { 2 + "components": { 3 + "schemas": { 4 + "Resource_v1_Schema": { 5 + "type": "object", 6 + "properties": { 7 + "status": { 8 + "$ref": "#/components/schemas/Resource_v1_StatusEnum" 9 + }, 10 + "health": { 11 + "$ref": "#/components/schemas/Resource_v1_HealthEnum" 12 + }, 13 + "name": { 14 + "type": "string" 15 + } 16 + } 17 + }, 18 + "Resource_v1_StatusEnum": { 19 + "type": "string", 20 + "enum": ["enabled", "disabled", "standby"] 21 + }, 22 + "Resource_v1_HealthEnum": { 23 + "type": "string", 24 + "enum": ["ok", "warning", "critical"] 25 + } 26 + } 27 + } 28 + }
+10
specs/json-schema-ref-parser/sibling-schema-multi-wrapper.json
··· 1 + { 2 + "components": { 3 + "schemas": { 4 + "Resource_Schema": { 5 + "description": "Wrapper with extra description", 6 + "$ref": "sibling-schema-multi-versioned.json#/components/schemas/Resource_v1_Schema" 7 + } 8 + } 9 + } 10 + }
+22
specs/json-schema-ref-parser/sibling-schema-root.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { "title": "Sibling Schema Test", "version": "1.0.0" }, 4 + "paths": { 5 + "/test": { 6 + "get": { 7 + "responses": { 8 + "200": { 9 + "description": "ok", 10 + "content": { 11 + "application/json": { 12 + "schema": { 13 + "$ref": "sibling-schema-wrapper.json#/components/schemas/Wrapper_Schema" 14 + } 15 + } 16 + } 17 + } 18 + } 19 + } 20 + } 21 + } 22 + }
+21
specs/json-schema-ref-parser/sibling-schema-versioned.json
··· 1 + { 2 + "components": { 3 + "schemas": { 4 + "Versioned_Schema": { 5 + "type": "object", 6 + "properties": { 7 + "name": { 8 + "type": "string" 9 + }, 10 + "status": { 11 + "$ref": "#/components/schemas/Versioned_MyEnum" 12 + } 13 + } 14 + }, 15 + "Versioned_MyEnum": { 16 + "type": "string", 17 + "enum": ["active", "inactive", "pending"] 18 + } 19 + } 20 + } 21 + }
+9
specs/json-schema-ref-parser/sibling-schema-wrapper.json
··· 1 + { 2 + "components": { 3 + "schemas": { 4 + "Wrapper_Schema": { 5 + "$ref": "sibling-schema-versioned.json#/components/schemas/Versioned_Schema" 6 + } 7 + } 8 + } 9 + }