@@ -9,6 +9,9 @@ import { GlobalContext } from "./types";
99import { parseRef } from "./utils" ;
1010
1111type PartialSchema = Record < string , any > ; // not a very accurate type, but this is easier to deal with before we know we’re dealing with a valid spec
12+ type SchemaMap = { [ url : string ] : PartialSchema } ;
13+
14+ export const VIRTUAL_JSON_URL = `file:///_json` ; // fake URL reserved for dynamic JSON
1215
1316function parseSchema ( schema : any , type : "YAML" | "JSON" ) {
1417 if ( type === "YAML" ) {
@@ -50,70 +53,88 @@ export function resolveSchema(url: string): URL {
5053
5154interface LoadOptions extends GlobalContext {
5255 rootURL : URL ;
53- schemas : { [ url : string ] : PartialSchema } ;
56+ schemas : SchemaMap ;
5457}
5558
5659// temporary cache for load()
5760let urlCache = new Set < string > ( ) ; // URL cache (prevent URLs from being loaded over and over)
5861
5962/** Load a schema from local path or remote URL */
60- export default async function load ( schemaURL : URL , options : LoadOptions ) : Promise < { [ url : string ] : PartialSchema } > {
61- if ( urlCache . has ( schemaURL . href ) ) return options . schemas ; // exit early if this has already been scanned
62- urlCache . add ( schemaURL . href ) ; // add URL to cache
63+ export default async function load (
64+ schema : URL | PartialSchema ,
65+ options : LoadOptions
66+ ) : Promise < { [ url : string ] : PartialSchema } > {
67+ const isJSON = schema instanceof URL === false ; // if this is dynamically-passed-in JSON, we’ll have to change a few things
68+ let schemaID = isJSON ? new URL ( VIRTUAL_JSON_URL ) . href : schema . href ;
6369
6470 const schemas = options . schemas ;
6571
66- let contents = "" ;
67- let contentType = "" ;
68-
69- if ( isFile ( schemaURL ) ) {
70- // load local
71- contents = await fs . promises . readFile ( schemaURL , "utf8" ) ;
72- contentType = mime . getType ( schemaURL . href ) || "" ;
73- } else {
74- // load remote
75- const headers = new Headers ( ) ;
76- headers . set ( "User-Agent" , "openapi-typescript" ) ;
77- if ( options . auth ) headers . set ( "Authorization" , options . auth ) ;
78- const res = await fetch ( schemaURL . href , { method : "GET" , headers } ) ;
79- contentType = res . headers . get ( "Content-Type" ) || "" ;
80- contents = await res . text ( ) ;
72+ // scenario 1: load schema from dynamic JSON
73+ if ( isJSON ) {
74+ schemas [ schemaID ] = schema ;
8175 }
76+ // scenario 2: fetch schema from URL (local or remote)
77+ else {
78+ if ( urlCache . has ( schemaID ) ) return options . schemas ; // exit early if this has already been scanned
79+ urlCache . add ( schemaID ) ; // add URL to cache
80+
81+ let contents = "" ;
82+ let contentType = "" ;
83+ const schemaURL = schema as URL ; // helps TypeScript
84+
85+ if ( isFile ( schemaURL ) ) {
86+ // load local
87+ contents = await fs . promises . readFile ( schemaURL , "utf8" ) ;
88+ contentType = mime . getType ( schemaID ) || "" ;
89+ } else {
90+ // load remote
91+ const headers = new Headers ( ) ;
92+ headers . set ( "User-Agent" , "openapi-typescript" ) ;
93+ if ( options . auth ) headers . set ( "Authorization" , options . auth ) ;
94+ const res = await fetch ( schemaID , { method : "GET" , headers } ) ;
95+ contentType = res . headers . get ( "Content-Type" ) || "" ;
96+ contents = await res . text ( ) ;
97+ }
8298
83- const isYAML = contentType === "application/openapi+yaml" || contentType === "text/yaml" ;
84- const isJSON =
85- contentType === "application/json" ||
86- contentType === "application/json5" ||
87- contentType === "application/openapi+json" ;
88- if ( isYAML ) {
89- schemas [ schemaURL . href ] = parseSchema ( contents , "YAML" ) ;
90- } else if ( isJSON ) {
91- schemas [ schemaURL . href ] = parseSchema ( contents , "JSON" ) ;
92- } else {
93- // if contentType is unknown, guess
94- try {
95- schemas [ schemaURL . href ] = parseSchema ( contents , "JSON" ) ;
96- } catch ( err1 ) {
99+ const isYAML = contentType === "application/openapi+yaml" || contentType === "text/yaml" ;
100+ const isJSON =
101+ contentType === "application/json" ||
102+ contentType === "application/json5" ||
103+ contentType === "application/openapi+json" ;
104+ if ( isYAML ) {
105+ schemas [ schemaID ] = parseSchema ( contents , "YAML" ) ;
106+ } else if ( isJSON ) {
107+ schemas [ schemaID ] = parseSchema ( contents , "JSON" ) ;
108+ } else {
109+ // if contentType is unknown, guess
97110 try {
98- schemas [ schemaURL . href ] = parseSchema ( contents , "YAML" ) ;
99- } catch ( err2 ) {
100- throw new Error ( `Unknown format${ contentType ? `: "${ contentType } "` : "" } . Only YAML or JSON supported.` ) ; // give up: unknown type
111+ schemas [ schemaID ] = parseSchema ( contents , "JSON" ) ;
112+ } catch ( err1 ) {
113+ try {
114+ schemas [ schemaID ] = parseSchema ( contents , "YAML" ) ;
115+ } catch ( err2 ) {
116+ throw new Error ( `Unknown format${ contentType ? `: "${ contentType } "` : "" } . Only YAML or JSON supported.` ) ; // give up: unknown type
117+ }
101118 }
102119 }
103120 }
104121
105122 // scan $refs, but don’t transform (load everything in parallel)
106123 const refPromises : Promise < any > [ ] = [ ] ;
107- schemas [ schemaURL . href ] = JSON . parse ( JSON . stringify ( schemas [ schemaURL . href ] ) , ( k , v ) => {
124+ schemas [ schemaID ] = JSON . parse ( JSON . stringify ( schemas [ schemaID ] ) , ( k , v ) => {
108125 if ( k !== "$ref" || typeof v !== "string" ) return v ;
109126
110127 const { url : refURL } = parseRef ( v ) ;
111128 if ( refURL ) {
112129 // load $refs (only if new) and merge subschemas with top-level schema
113- const nextURL =
114- refURL . startsWith ( "http://" ) || refURL . startsWith ( "https://" )
115- ? new URL ( refURL )
116- : new URL ( slash ( refURL ) , schemaURL ) ;
130+ const isRemoteURL = refURL . startsWith ( "http://" ) || refURL . startsWith ( "https://" ) ;
131+
132+ // if this is dynamic JSON, we have no idea how to resolve relative URLs, so throw here
133+ if ( isJSON && ! isRemoteURL ) {
134+ throw new Error ( `Can’t load URL "${ refURL } " from dynamic JSON. Load this schema from a URL instead.` ) ;
135+ }
136+
137+ const nextURL = isRemoteURL ? new URL ( refURL ) : new URL ( slash ( refURL ) , schema as URL ) ;
117138 refPromises . push (
118139 load ( nextURL , options ) . then ( ( subschemas ) => {
119140 for ( const subschemaURL of Object . keys ( subschemas ) ) {
@@ -128,7 +149,7 @@ export default async function load(schemaURL: URL, options: LoadOptions): Promis
128149 await Promise . all ( refPromises ) ;
129150
130151 // transform $refs once, at the root schema, after all have been scanned & downloaded (much easier to do here when we have the context)
131- if ( schemaURL . href === options . rootURL . href ) {
152+ if ( schemaID === options . rootURL . href ) {
132153 for ( const subschemaURL of Object . keys ( schemas ) ) {
133154 // transform $refs in schema
134155 schemas [ subschemaURL ] = JSON . parse ( JSON . stringify ( schemas [ subschemaURL ] ) , ( k , v ) => {
0 commit comments