@@ -4,7 +4,9 @@ import { readFile } from "fs/promises";
4
4
import yaml from "js-yaml" ;
5
5
import { marked } from "marked" ;
6
6
import { dirname , normalize , relative , resolve } from "path" ;
7
+ import * as z from "zod" ;
7
8
import { mapAsync } from "./array.js" ;
9
+ import { SpecModelError } from "./spec-model-error.js" ;
8
10
import { embedError } from "./spec-model.js" ;
9
11
import { Tag } from "./tag.js" ;
10
12
@@ -18,6 +20,21 @@ import { Tag } from "./tag.js";
18
20
*/
19
21
export const TagMatchRegex = / y a m l .* \$ \( t a g \) ? = = ? ( [ " ' ] ) ( .* ?) \1/ ;
20
22
23
+ // Example: "foo.json"
24
+ const jsonFileSchema = z . string ( ) . regex ( / \. j s o n $ / i) ;
25
+
26
+ // Examples:
27
+ // {}
28
+ // {"input-file": "foo.json"}
29
+ // {"input-file": ["foo.json", "bar.json"]}
30
+ const inputFileSchema = z . object ( {
31
+ "input-file" : z
32
+ // May be undefined, a single json filename, or an array of json filenames
33
+ . optional ( z . union ( [ jsonFileSchema , z . array ( jsonFileSchema ) ] ) )
34
+ // Normalize single string to array of string. Don't change 'undefined'.
35
+ . transform ( ( value ) => ( typeof value === "string" ? [ value ] : value ) ) ,
36
+ } ) ;
37
+
21
38
export class Readme {
22
39
/**
23
40
* Content of `readme.md`, either loaded from `#path` or passed in via `options`.
@@ -53,12 +70,14 @@ export class Readme {
53
70
* @param {import('./logger.js').ILogger } [options.logger]
54
71
* @param {SpecModel } [options.specModel]
55
72
*/
56
- constructor ( path , options ) {
57
- this . #path = resolve ( options ?. specModel ?. folder ?? "" , path ) ;
73
+ constructor ( path , options = { } ) {
74
+ const { content, logger, specModel } = options ;
75
+
76
+ this . #path = resolve ( specModel ?. folder ?? "" , path ) ;
58
77
59
- this . #content = options ?. content ;
60
- this . #logger = options ?. logger ;
61
- this . #specModel = options ?. specModel ;
78
+ this . #content = content ;
79
+ this . #logger = logger ;
80
+ this . #specModel = specModel ;
62
81
}
63
82
64
83
/**
@@ -127,11 +146,31 @@ export class Readme {
127
146
const obj = /** @type {any } */ ( yaml . load ( block . text , { schema : yaml . FAILSAFE_SCHEMA } ) ) ;
128
147
129
148
if ( ! obj ) {
130
- this . #logger?. debug ( `No yaml object found for tag ${ tagName } in ${ this . #path} ` ) ;
149
+ this . #logger?. debug ( `No YAML object found for tag ${ tagName } in ${ this . #path} ` ) ;
131
150
continue ;
132
151
}
133
152
134
- if ( ! obj [ "input-file" ] ) {
153
+ let parsedObj ;
154
+ try {
155
+ parsedObj = inputFileSchema . parse ( obj ) ;
156
+ } catch ( error ) {
157
+ if ( error instanceof z . ZodError ) {
158
+ throw new SpecModelError (
159
+ `Unable to parse input-file YAML for tag ${ tagName } in ${ this . #path} ` ,
160
+ {
161
+ source : this . #path,
162
+ readme : this . #path,
163
+ tag : tagName ,
164
+ cause : error ,
165
+ } ,
166
+ ) ;
167
+ } /* v8 ignore start: defensive rethrow */ else {
168
+ throw error ;
169
+ }
170
+ /* v8 ignore end */
171
+ }
172
+
173
+ if ( ! parsedObj [ "input-file" ] ) {
135
174
// The yaml block does not contain an input-file key
136
175
continue ;
137
176
}
@@ -149,10 +188,7 @@ export class Readme {
149
188
throw new Error ( message ) ;
150
189
}
151
190
152
- // It's possible for input-file to be a string or an array
153
- const inputFilePaths = Array . isArray ( obj [ "input-file" ] )
154
- ? obj [ "input-file" ]
155
- : [ obj [ "input-file" ] ] ;
191
+ const inputFilePaths = parsedObj [ "input-file" ] ;
156
192
157
193
const swaggerPathsResolved = inputFilePaths
158
194
. map ( ( p ) => Readme . #normalizeSwaggerPath( p ) )
@@ -207,7 +243,9 @@ export class Readme {
207
243
* @param {ToJSONOptions } [options]
208
244
* @returns {Promise<Object> }
209
245
*/
210
- async toJSONAsync ( options ) {
246
+ async toJSONAsync ( options = { } ) {
247
+ const { relativePaths } = options ;
248
+
211
249
return await embedError ( async ( ) => {
212
250
const tags = await mapAsync (
213
251
[ ...( await this . getTags ( ) ) . values ( ) ] . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ,
@@ -216,7 +254,7 @@ export class Readme {
216
254
217
255
return {
218
256
path :
219
- options ?. relativePaths && this . #specModel
257
+ relativePaths && this . #specModel
220
258
? relative ( this . #specModel. folder , this . #path)
221
259
: this . #path,
222
260
globalConfig : await this . getGlobalConfig ( ) ,
0 commit comments