@@ -9,6 +9,9 @@ import { GlobalContext } from "./types";
9
9
import { parseRef } from "./utils" ;
10
10
11
11
type 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
12
15
13
16
function parseSchema ( schema : any , type : "YAML" | "JSON" ) {
14
17
if ( type === "YAML" ) {
@@ -50,70 +53,88 @@ export function resolveSchema(url: string): URL {
50
53
51
54
interface LoadOptions extends GlobalContext {
52
55
rootURL : URL ;
53
- schemas : { [ url : string ] : PartialSchema } ;
56
+ schemas : SchemaMap ;
54
57
}
55
58
56
59
// temporary cache for load()
57
60
let urlCache = new Set < string > ( ) ; // URL cache (prevent URLs from being loaded over and over)
58
61
59
62
/** 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 ;
63
69
64
70
const schemas = options . schemas ;
65
71
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 ;
81
75
}
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
+ }
82
98
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
97
110
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
+ }
101
118
}
102
119
}
103
120
}
104
121
105
122
// scan $refs, but don’t transform (load everything in parallel)
106
123
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 ) => {
108
125
if ( k !== "$ref" || typeof v !== "string" ) return v ;
109
126
110
127
const { url : refURL } = parseRef ( v ) ;
111
128
if ( refURL ) {
112
129
// 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 ) ;
117
138
refPromises . push (
118
139
load ( nextURL , options ) . then ( ( subschemas ) => {
119
140
for ( const subschemaURL of Object . keys ( subschemas ) ) {
@@ -128,7 +149,7 @@ export default async function load(schemaURL: URL, options: LoadOptions): Promis
128
149
await Promise . all ( refPromises ) ;
129
150
130
151
// 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 ) {
132
153
for ( const subschemaURL of Object . keys ( schemas ) ) {
133
154
// transform $refs in schema
134
155
schemas [ subschemaURL ] = JSON . parse ( JSON . stringify ( schemas [ subschemaURL ] ) , ( k , v ) => {
0 commit comments