• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

touchifyapp / fastify-oapi / 4662355291

pending completion
4662355291

push

github

Maxime LUCE
feat!: upgrade plugin to fastify v4

191 of 214 branches covered (89.25%)

Branch coverage included in aggregate %.

13 of 13 new or added lines in 5 files covered. (100.0%)

269 of 283 relevant lines covered (95.05%)

307.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.37
/lib/parser.ts
1
import type { FastifySchema, RouteHandler, HTTPMethods } from "fastify";
2
import type { oas30 } from "openapi3-ts";
5✔
3

5✔
4
import { $RefParser } from "@apidevtools/json-schema-ref-parser";
5✔
5
import { omit } from "./util";
5✔
6

7
const HttpOperations = new Set(["delete", "get", "head", "patch", "post", "put", "options"]);
8

49✔
9
export type $Refs = $RefParser["$refs"];
49!
10

1✔
11
export interface ParsedConfig {
12
    $refs: $Refs;
48✔
13
    shared: oas30.SchemaObject | undefined;
48✔
14
    generic: Omit<oas30.OpenAPIObject, "paths">;
15
    routes: ParsedRoute[];
16
    prefix?: string;
17
}
18

19
export interface ParsedRoute {
48✔
20
    method: HTTPMethods;
48✔
21
    url: string;
265✔
22
    schema: FastifySchema;
48✔
23
    operationId: string;
24
    openapiSource: oas30.OperationObject;
25
    wildcard?: string;
217✔
26
    handler?: RouteHandler;
27
}
28

48✔
29
export default async function parse(specOrPath: string | oas30.OpenAPIObject): Promise<ParsedConfig> {
30
    const spec = await bundleSpecification(specOrPath); //await dereference(specOrPath);
5✔
31

32
    if (!spec?.openapi?.startsWith("3.0")) {
33
        throw new Error("The 'specification' parameter must contain a valid version 3.0.x specification");
48✔
34
    }
3✔
35

36
    const $refs = await $RefParser.resolve(spec);
45✔
37
    const config: ParsedConfig = {
38
        $refs,
39
        shared: createSharedSchema(spec, $refs),
40
        generic: {} as any,
135!
41
        routes: [],
42
    };
43

44
    const keys = Object.keys(spec) as Array<keyof oas30.OpenAPIObject>;
45
    keys.forEach((key) => {
46
        if (key === "paths") {
48✔
47
            processPaths(config, spec.paths);
48✔
48
        } else {
430✔
49
            config.generic[key] = spec[key];
430✔
50
        }
430✔
51
    });
430✔
52

9✔
53
    return config;
54
}
430✔
55

523✔
56
//#region Parser
523✔
57

514✔
58
type OpenAPIObjectWithDefs = oas30.OpenAPIObject & { definitions?: Record<string, oas30.SchemaObject> };
59
type SharedSchema = oas30.SchemaObject & {
60
    $id: string;
61
    definitions: Record<string, oas30.SchemaObject>;
62
};
63

64
function createSharedSchema(spec: OpenAPIObjectWithDefs, $refs: $Refs): SharedSchema | undefined {
514!
65
    if (!spec.definitions && !spec.components?.schemas) {
×
66
        return;
67
    }
514✔
68

69
    return {
70
        $id: "urn:schema:api",
71
        definitions: {
542✔
72
            ...parseSchemaItems(spec.definitions, { $refs }),
73
            ...parseSchemaItems(spec.components?.schemas, { $refs }),
74
        },
514✔
75
    };
76
}
77

78
/** Process OpenAPI Paths. */
514✔
79
function processPaths(config: ParsedConfig, paths: Record<string, oas30.PathItemObject>): void {
514✔
80
    const copyItems = ["summary", "description"];
514✔
81

358✔
82
    for (const path in paths) {
83
        const genericSchema = {};
514✔
84
        const pathItem = paths[path];
514✔
85

126✔
86
        copyProps(pathItem, genericSchema, copyItems);
87

514✔
88
        if (Array.isArray(pathItem.parameters)) {
514✔
89
            parseParameters(config, genericSchema, pathItem.parameters);
185✔
90
        }
91

514✔
92
        Object.keys(pathItem).forEach((verb) => {
93
            const operation = pathItem[verb as keyof oas30.PathItemObject];
94
            if (isHttpVerb(verb) && operation) {
95
                processOperation(config, path, verb, operation, pathItem, genericSchema);
367✔
96
            }
367✔
97
        });
367✔
98
    }
367✔
99
}
423✔
100

423✔
101
/** Build fastify RouteSchema and add it to routes. */
102
function processOperation(
213✔
103
    config: ParsedConfig,
213✔
104
    path: string,
105
    method: string,
155✔
106
    operation: oas30.OperationObject,
155✔
107
    pathItem: oas30.PathItemObject,
108
    genericSchema: FastifySchema
55✔
109
): void {
55✔
110
    if (!operation) {
111
        return;
112
    }
367✔
113

213✔
114
    const route: ParsedRoute = {
115
        method: method.toUpperCase() as HTTPMethods,
367✔
116
        ...makeURL(path, pathItem, operation, config),
113✔
117
        schema: parseOperationSchema(config, genericSchema, operation),
118
        operationId: operation.operationId || makeOperationId(method, path),
367✔
119
        openapiSource: operation,
55✔
120
    };
121

122
    config.routes.push(route);
123
}
124

125
/** Build fastify RouteSchema based on OpenAPI Operation */
381✔
126
function parseOperationSchema(
381✔
127
    config: ParsedConfig,
381✔
128
    genericSchema: FastifySchema,
381✔
129
    operation: oas30.OperationObject
423✔
130
): FastifySchema {
423✔
131
    const schema = Object.assign({}, genericSchema);
28✔
132

133
    copyProps(operation, schema, ["tags", "summary", "description", "operationId"]);
423✔
134

423✔
135
    if (operation.parameters) {
423✔
136
        parseParameters(config, schema, operation.parameters);
3✔
137
    }
138

423✔
139
    const body = parseBody(config, operation.requestBody);
278✔
140
    if (body) {
141
        schema.body = body;
142
    }
381✔
143

144
    const response = parseResponses(config, operation.responses);
145
    if (response) {
146
        schema.response = response;
147
    }
148

149
    return schema;
150
}
151

152
/** Parse Open API params for Query/Params/Headers and include them into FastifySchema. */
153
function parseParameters(
154
    config: ParsedConfig,
155
    schema: FastifySchema,
156
    parameters: Array<oas30.ParameterObject | oas30.ReferenceObject>
514✔
157
): void {
514✔
158
    const params: oas30.ParameterObject[] = [];
514✔
159
    const querystring: oas30.ParameterObject[] = [];
730✔
160
    const headers: oas30.ParameterObject[] = [];
730✔
161

56✔
162
    parameters.forEach((item) => {
163
        item = resolveReference(item, config);
730✔
164
        switch (item.in) {
191✔
165
            case "path":
191✔
166
                params.push(item);
167
                break;
168
            case "query":
514✔
169
                querystring.push(item);
170
                break;
171
            case "header":
172
                headers.push(item);
173
                break;
1,244✔
174
        }
1,244✔
175
    });
317✔
176

177
    if (params.length > 0) {
178
        schema.params = parseParams(config, schema.params as oas30.SchemaObject, params);
179
    }
180

740!
181
    if (querystring.length > 0) {
×
182
        schema.querystring = parseParams(config, schema.querystring as oas30.SchemaObject, querystring);
740✔
183
    }
184

185
    if (headers.length > 0) {
3,901✔
186
        schema.headers = parseParams(config, schema.headers as oas30.SchemaObject, headers);
72✔
187
    }
188
}
3,829✔
189

213✔
190
/** Parse Open API params for Query/Params/Headers. */
191
function parseParams(
3,714✔
192
    config: ParsedConfig,
1,744✔
193
    base: oas30.SchemaObject | undefined,
1,744✔
194
    parameters: oas30.ParameterObject[]
6✔
195
): oas30.SchemaObject {
6!
196
    const properties: Record<string, oas30.SchemaObject | oas30.ReferenceObject> = {};
6✔
197

198
    const required: string[] = [];
199
    const baseRequired = new Set(base?.required ?? []);
×
200

201
    parameters.forEach((item) => {
202
        let itemName = item.name;
1,744✔
203
        if (item["x-wildcard"] === true) {
204
            itemName = "*";
311✔
205
        }
206

1,433✔
207
        properties[itemName] = parseSchema(item.schema, config);
1,433✔
208

2,858✔
209
        copyProps(item, properties[itemName], ["description"]);
210

1,433✔
211
        if (baseRequired.has(itemName)) {
212
            baseRequired.delete(itemName);
1,970✔
213
        }
214

215
        if (item.required) {
216
            required.push(itemName);
217
        }
218
    });
219

220
    return base
221
        ? {
28✔
222
              type: "object",
28✔
223
              properties: Object.assign({}, base.properties, properties),
224
              required: [...baseRequired, ...required],
225
          }
226
        : {
28✔
227
              type: "object",
228
              properties,
229
              required,
230
          };
231
}
49✔
232

233
/** Parse Open API responses */
234
function parseResponses(
235
    config: ParsedConfig,
1,897✔
236
    responses?: Record<string, oas30.ResponseObject | oas30.ReferenceObject>
1,838✔
237
): Record<string, oas30.SchemaObject | oas30.ReferenceObject> | null {
238
    const result: Record<string, oas30.SchemaObject | oas30.ReferenceObject> = {};
59✔
239

240
    let hasResponse = false;
241
    for (let httpCode in responses) {
242
        const body = parseBody(config, responses[httpCode]);
3,647✔
243

244
        if (httpCode === "default") {
245
            httpCode = "xxx";
246
        }
247

514✔
248
        if (body) {
213✔
249
            result[httpCode] = body;
213!
250
            hasResponse = true;
28✔
251
        }
28✔
252
    }
253

185✔
254
    return hasResponse ? result : null;
255
}
514✔
256

257
/** Parse Open API content contract to prepare RouteSchema */
258
function parseBody(
259
    config: ParsedConfig,
1,367✔
260
    body?: oas30.RequestBodyObject | oas30.ResponseObject | oas30.ReferenceObject
3,339✔
261
): oas30.SchemaObject | oas30.ReferenceObject | undefined {
1,588✔
262
    body = resolveReference(body, config);
263

264
    if (body?.content?.["application/json"]) {
265
        return parseSchema(body.content["application/json"].schema, config);
266
    }
84✔
267
}
268

269
/** Parse a schema and inject shared reference if needed. */
523✔
270
function parseSchema(
271
    schema: oas30.SchemaObject | oas30.ReferenceObject | undefined,
272
    config: { $refs: $Refs }
213✔
273
): oas30.SchemaObject | oas30.ReferenceObject {
227✔
274
    if (!schema) return {};
275
    return parseSchemaItems(schema, config);
276
}
277

278
function parseSchemaItems(item: any, config: { $refs: $Refs }): any {
279
    if (!item) {
280
        return item;
281
    }
282

283
    if (Array.isArray(item)) {
284
        return item.map((c) => parseSchemaItems(c, config));
285
    }
286

287
    if (typeof item === "object") {
288
        const { "x-partial": xPartial, ...itemSchema } = item;
289

290
        if (xPartial) {
291
            const schema = isReference(itemSchema) ? resolveReference(itemSchema, config) : itemSchema;
292

293
            if (schema.required) {
294
                item = omit(schema, ["required"]);
295
            } else {
296
                item = itemSchema;
297
            }
298
        }
299

300
        if (isReference(item)) {
301
            // return { $ref: "urn:schema:api" + item.$ref };
302
            return { $ref: item.$ref.replace("#/components/schemas", "urn:schema:api#/definitions") };
303
        }
304

305
        const res: any = {};
306
        for (const key in item) {
307
            res[key] = parseSchemaItems(item[key], config);
308
        }
309

310
        return res;
311
    }
312

313
    return item;
314
}
315

316
//#endregion
317

318
//#region Utilities
319

320
/**
321
 * Make human-readable operation id.
322
 * @example get /user/{name}  becomes getUserByName
323
 */
324
function makeOperationId(method: string, path: string): string {
325
    const parts = path.split("/").slice(1);
326

327
    return (
328
        method +
329
        parts
330
            .map(firstUpper)
331
            .join("")
332
            .replace(/{(\w+)}/g, (_, p1) => "By" + firstUpper(p1))
333
            .replace(/[^a-z]/gi, "")
334
    );
335
}
336

337
/** Bundle Specification file. */
338
async function bundleSpecification(spec: string | oas30.OpenAPIObject): Promise<oas30.OpenAPIObject> {
339
    return (await $RefParser.bundle(spec)) as oas30.OpenAPIObject;
340
}
341

342
/** Resolves external reference */
343
function resolveReference<T>(obj: T | oas30.ReferenceObject, { $refs }: { $refs: $Refs }): T {
344
    if (!isReference(obj)) {
345
        return obj;
346
    }
347

348
    return $refs.get(obj.$ref) as unknown as T;
349
}
350

351
/** Check if specified Object is a reference. */
352
function isReference(obj: any): obj is oas30.ReferenceObject {
353
    return obj && "$ref" in obj;
354
}
355

356
/** Adjust URLs from OpenAPI to fastify. (openapi: 'path/{param}' => fastify: 'path/:param'). */
357
function makeURL(
358
    path: string,
359
    pathItem: oas30.PathItemObject,
360
    operation: oas30.OperationObject,
361
    config: ParsedConfig
362
): { url: string; wildcard?: string } {
363
    let wildcard: string | undefined;
364

365
    const url = path.replace(/{(\w+)}/g, (_, paramName) => {
366
        const param = findParameter(paramName, pathItem, operation, config);
367
        if (param?.["x-wildcard"] === true) {
368
            wildcard = paramName;
369
            return "*";
370
        }
371

372
        return `:${paramName}`;
373
    });
374

375
    return { url, wildcard };
376
}
377

378
/** Copy the given list of properties from source to target. */
379
function copyProps(source: Record<string, any>, target: Record<string, any>, list: string[]): void {
380
    list.forEach((item) => {
381
        if (source[item]) {
382
            target[item] = source[item];
383
        }
384
    });
385
}
386

387
function firstUpper(str: string): string {
388
    return str.substr(0, 1).toUpperCase() + str.substr(1);
389
}
390

391
function isHttpVerb(str: string): str is "get" | "put" | "post" | "delete" | "options" | "head" | "trace" {
392
    return HttpOperations.has(str);
393
}
394

395
function findParameter(
396
    paramName: string,
397
    pathItem: oas30.PathItemObject,
398
    operation: oas30.OperationObject,
399
    config: ParsedConfig
400
): oas30.ParameterObject | undefined {
401
    const parameters = [...(pathItem.parameters || []), ...(operation.parameters || [])];
402

403
    return parameters.map((p) => resolveReference(p, config)).find((p) => p.in === "path" && p.name === paramName);
404
}
405

406
//#endregion
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc