openapi-contract-kit 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +13 -5
  3. package/dist/bin/openapi-contract-kit.d.ts +2 -0
  4. package/dist/bin/openapi-contract-kit.js +3 -0
  5. package/dist/src/cli/formatOutput.d.ts +6 -0
  6. package/dist/src/cli/formatOutput.js +32 -0
  7. package/dist/src/generator/config.d.ts +3 -0
  8. package/dist/src/generator/config.js +72 -0
  9. package/dist/src/generator/documents.d.ts +11 -0
  10. package/dist/src/generator/documents.js +92 -0
  11. package/dist/src/generator/emitEndpoints.d.ts +2 -0
  12. package/{src/generator/emitEndpoints.mjs → dist/src/generator/emitEndpoints.js} +48 -80
  13. package/dist/src/generator/emitMakers.d.ts +2 -0
  14. package/dist/src/generator/emitMakers.js +328 -0
  15. package/dist/src/generator/emitTypes.d.ts +3 -0
  16. package/dist/src/generator/emitTypes.js +87 -0
  17. package/dist/src/generator/generateOpenApiRuntime.d.ts +3 -0
  18. package/dist/src/generator/generateOpenApiRuntime.js +41 -0
  19. package/dist/src/generator/model.d.ts +2 -0
  20. package/dist/src/generator/model.js +278 -0
  21. package/dist/src/generator/schemaModel.d.ts +9 -0
  22. package/dist/src/generator/schemaModel.js +408 -0
  23. package/dist/src/generator/types.d.ts +82 -0
  24. package/dist/src/generator/writeOutput.d.ts +5 -0
  25. package/dist/src/generator/writeOutput.js +141 -0
  26. package/dist/src/index.d.ts +2 -0
  27. package/dist/src/index.js +1 -0
  28. package/dist/src/runtime.d.ts +14 -0
  29. package/dist/src/runtime.js +1 -0
  30. package/package.json +30 -18
  31. package/bin/openapi-contract-kit.mjs +0 -5
  32. package/src/generator/config.mjs +0 -92
  33. package/src/generator/documents.mjs +0 -119
  34. package/src/generator/emitMakers.mjs +0 -459
  35. package/src/generator/emitTypes.mjs +0 -104
  36. package/src/generator/generateOpenApiRuntime.mjs +0 -48
  37. package/src/generator/model.mjs +0 -415
  38. package/src/generator/schemaModel.mjs +0 -497
  39. package/src/generator/writeOutput.mjs +0 -196
  40. package/src/index.d.ts +0 -16
  41. package/src/index.mjs +0 -4
  42. package/src/runtime.d.ts +0 -14
  43. /package/{src/runtime.mjs → dist/src/generator/types.js} +0 -0
@@ -1,415 +0,0 @@
1
- import { resolve } from 'node:path';
2
-
3
- import {
4
- DocumentStore,
5
- escapePointerSegment,
6
- locationOf,
7
- pointerChild,
8
- requireRecord,
9
- } from './documents.mjs';
10
- import { SchemaRegistry, toPascalIdentifier } from './schemaModel.mjs';
11
-
12
- const HTTP_METHODS = [
13
- 'delete',
14
- 'get',
15
- 'head',
16
- 'options',
17
- 'patch',
18
- 'post',
19
- 'put',
20
- 'trace',
21
- ];
22
- const JSON_MEDIA_TYPE = 'application/json';
23
-
24
- class ModelBuilder {
25
- #documents = new DocumentStore();
26
- #registry;
27
- #rootDocument;
28
- #rootPath;
29
-
30
- async build(specPath) {
31
- this.#rootPath = resolve(specPath);
32
- this.#rootDocument = await this.#documents.load(this.#rootPath);
33
- this.#registry = new SchemaRegistry(this.#documents, this.#rootPath);
34
- this.#validateOpenApiRoot();
35
- this.#seedComponentSchemas();
36
- await this.#seedResponseSchemas();
37
- const operationDrafts = this.#collectOperationDrafts();
38
- const operations = await this.#buildOperations(operationDrafts);
39
-
40
- return {
41
- operations: operations.sort((left, right) =>
42
- left.name.localeCompare(right.name)
43
- ),
44
- schemas: await this.#registry.build(),
45
- };
46
- }
47
-
48
- #validateOpenApiRoot() {
49
- const version = this.#rootDocument.openapi;
50
-
51
- if (typeof version !== 'string' || !version.startsWith('3.1.')) {
52
- throw new Error(
53
- `OpenAPI document "${this.#rootPath}" must use OpenAPI 3.1`
54
- );
55
- }
56
- requireRecord(
57
- this.#rootDocument.paths,
58
- locationOf(this.#rootPath, '/paths'),
59
- 'OpenAPI paths'
60
- );
61
- }
62
-
63
- #seedComponentSchemas() {
64
- const schemas = this.#rootDocument.components?.schemas;
65
-
66
- if (schemas === undefined) {
67
- return;
68
- }
69
- requireRecord(
70
- schemas,
71
- locationOf(this.#rootPath, '/components/schemas'),
72
- 'OpenAPI component schemas'
73
- );
74
-
75
- for (const componentName of Object.keys(schemas).sort()) {
76
- this.#registry.register(
77
- Reflect.get(schemas, componentName),
78
- {
79
- documentPath: this.#rootPath,
80
- pointer: `/components/schemas/${escapePointerSegment(componentName)}`,
81
- },
82
- componentName
83
- );
84
- }
85
- }
86
-
87
- async #seedResponseSchemas() {
88
- const responses = this.#rootDocument.components?.responses;
89
-
90
- if (responses === undefined) {
91
- return;
92
- }
93
- requireRecord(
94
- responses,
95
- locationOf(this.#rootPath, '/components/responses'),
96
- 'OpenAPI component responses'
97
- );
98
-
99
- for (const responseName of Object.keys(responses).sort()) {
100
- const context = {
101
- documentPath: this.#rootPath,
102
- pointer: `/components/responses/${escapePointerSegment(responseName)}`,
103
- };
104
- const resolved = await this.#resolveReferenceObject(
105
- Reflect.get(responses, responseName),
106
- context,
107
- 'Response'
108
- );
109
- const body = this.#readJsonBody(
110
- resolved.value,
111
- resolved.context,
112
- 'Response'
113
- );
114
-
115
- if (body !== null) {
116
- this.#registry.register(body.schema, body.context, responseName);
117
- }
118
- }
119
- }
120
-
121
- #collectOperationDrafts() {
122
- const drafts = [];
123
- const operationIds = new Set();
124
-
125
- for (const path of Object.keys(this.#rootDocument.paths).sort()) {
126
- const pathPointer = `/paths/${escapePointerSegment(path)}`;
127
- const pathItem = requireRecord(
128
- Reflect.get(this.#rootDocument.paths, path),
129
- locationOf(this.#rootPath, pathPointer),
130
- 'Path item'
131
- );
132
-
133
- if (pathItem.$ref !== undefined) {
134
- throw new Error(`Referenced path items are unsupported at ${path}`);
135
- }
136
- if (
137
- Array.isArray(pathItem.parameters) &&
138
- pathItem.parameters.length > 0
139
- ) {
140
- throw new Error(`Path parameters are unsupported at ${path}`);
141
- }
142
-
143
- for (const method of HTTP_METHODS) {
144
- if (pathItem[method] === undefined) {
145
- continue;
146
- }
147
- const operation = requireRecord(
148
- pathItem[method],
149
- locationOf(this.#rootPath, `${pathPointer}/${method}`),
150
- 'Operation'
151
- );
152
- const operationId = operation.operationId;
153
-
154
- if (typeof operationId !== 'string' || operationId.length === 0) {
155
- throw new Error(
156
- `Missing operationId for ${method.toUpperCase()} ${path}`
157
- );
158
- }
159
- if (operationIds.has(operationId)) {
160
- throw new Error(`Duplicate operationId "${operationId}"`);
161
- }
162
- if (
163
- Array.isArray(operation.parameters) &&
164
- operation.parameters.length > 0
165
- ) {
166
- throw new Error(
167
- `Operation parameters are unsupported for ${operationId}`
168
- );
169
- }
170
-
171
- operationIds.add(operationId);
172
- drafts.push({ method, operation, operationId, path, pathPointer });
173
- }
174
- }
175
-
176
- const leafCounts = new Map();
177
- for (const draft of drafts) {
178
- const segments = draft.operationId
179
- .split(/[^A-Za-z0-9_$]+/u)
180
- .filter(Boolean);
181
- const leaf = toPascalIdentifier(
182
- segments.at(-1) ?? draft.operationId,
183
- 'Operation'
184
- );
185
- draft.leafName = leaf;
186
- leafCounts.set(
187
- leaf.toLowerCase(),
188
- (leafCounts.get(leaf.toLowerCase()) ?? 0) + 1
189
- );
190
- }
191
-
192
- const names = new Set();
193
- for (const draft of drafts) {
194
- draft.name =
195
- leafCounts.get(draft.leafName.toLowerCase()) === 1
196
- ? draft.leafName
197
- : toPascalIdentifier(draft.operationId, 'Operation');
198
- const key = draft.name.toLowerCase();
199
- if (names.has(key)) {
200
- throw new Error(`Operation name collision for "${draft.name}"`);
201
- }
202
- names.add(key);
203
- }
204
-
205
- return drafts;
206
- }
207
-
208
- async #buildOperations(drafts) {
209
- const operations = [];
210
-
211
- for (const draft of drafts) {
212
- const operationPointer = `${draft.pathPointer}/${draft.method}`;
213
- const context = {
214
- documentPath: this.#rootPath,
215
- pointer: operationPointer,
216
- };
217
- operations.push({
218
- method: draft.method.toUpperCase(),
219
- name: draft.name,
220
- operationId: draft.operationId,
221
- path: draft.path,
222
- request: await this.#buildRequest(draft.operation, context, draft.name),
223
- responses: await this.#buildResponses(
224
- draft.operation,
225
- context,
226
- draft.name
227
- ),
228
- });
229
- }
230
-
231
- return operations;
232
- }
233
-
234
- async #buildRequest(operation, context, operationName) {
235
- if (operation.requestBody === undefined) {
236
- return { schemaName: null };
237
- }
238
-
239
- const requestContext = {
240
- documentPath: context.documentPath,
241
- pointer: pointerChild(context.pointer, 'requestBody'),
242
- };
243
- const resolved = await this.#resolveReferenceObject(
244
- operation.requestBody,
245
- requestContext,
246
- 'Request body'
247
- );
248
-
249
- if (resolved.value.required !== true) {
250
- throw new Error(
251
- `Optional request bodies are unsupported for ${operationName}`
252
- );
253
- }
254
- const body = this.#readJsonBody(
255
- resolved.value,
256
- resolved.context,
257
- 'Request body'
258
- );
259
- if (body === null) {
260
- throw new Error(`Request body for ${operationName} has no JSON schema`);
261
- }
262
-
263
- return {
264
- schemaName: await this.#registry.schemaNameFor(
265
- body.schema,
266
- body.context,
267
- `${operationName}Request`
268
- ),
269
- };
270
- }
271
-
272
- async #buildResponses(operation, context, operationName) {
273
- const responsesPointer = pointerChild(context.pointer, 'responses');
274
- const responses = requireRecord(
275
- operation.responses,
276
- locationOf(context.documentPath, responsesPointer),
277
- 'Operation responses'
278
- );
279
- const result = [];
280
-
281
- for (const statusKey of Object.keys(responses).sort(
282
- (left, right) => Number(left) - Number(right)
283
- )) {
284
- if (!/^\d{3}$/u.test(statusKey)) {
285
- throw new Error(
286
- `Response status "${statusKey}" is unsupported for ${operationName}`
287
- );
288
- }
289
- const status = Number(statusKey);
290
- if (status < 100 || status > 599) {
291
- throw new Error(
292
- `Invalid response status "${statusKey}" for ${operationName}`
293
- );
294
- }
295
- const responseContext = {
296
- documentPath: context.documentPath,
297
- pointer: pointerChild(responsesPointer, statusKey),
298
- };
299
- const resolved = await this.#resolveReferenceObject(
300
- Reflect.get(responses, statusKey),
301
- responseContext,
302
- 'Response'
303
- );
304
- const body = this.#readJsonBody(
305
- resolved.value,
306
- resolved.context,
307
- 'Response'
308
- );
309
- const schemaName =
310
- body === null
311
- ? null
312
- : await this.#registry.schemaNameFor(
313
- body.schema,
314
- body.context,
315
- `${operationName}Response${statusKey}`
316
- );
317
-
318
- result.push({
319
- isSuccess: status >= 200 && status <= 299,
320
- schemaName,
321
- status,
322
- });
323
- }
324
-
325
- if (result.length === 0) {
326
- throw new Error(`Operation ${operationName} must document a response`);
327
- }
328
-
329
- return result;
330
- }
331
-
332
- #readJsonBody(value, context, label) {
333
- if (value.content === undefined) {
334
- return null;
335
- }
336
- const content = requireRecord(
337
- value.content,
338
- locationOf(
339
- context.documentPath,
340
- pointerChild(context.pointer, 'content')
341
- ),
342
- `${label} content`
343
- );
344
- const mediaTypes = Object.keys(content);
345
-
346
- if (mediaTypes.length === 0) {
347
- return null;
348
- }
349
- if (mediaTypes.length !== 1 || mediaTypes[0] !== JSON_MEDIA_TYPE) {
350
- throw new Error(
351
- `${label} at ${locationOf(context.documentPath, context.pointer)} must use only ${JSON_MEDIA_TYPE}`
352
- );
353
- }
354
- const media = requireRecord(
355
- content[JSON_MEDIA_TYPE],
356
- locationOf(
357
- context.documentPath,
358
- pointerChild(pointerChild(context.pointer, 'content'), JSON_MEDIA_TYPE)
359
- ),
360
- `${label} media type`
361
- );
362
- if (media.schema === undefined) {
363
- throw new Error(
364
- `${label} at ${locationOf(context.documentPath, context.pointer)} has no schema`
365
- );
366
- }
367
-
368
- return {
369
- context: {
370
- documentPath: context.documentPath,
371
- pointer: pointerChild(
372
- pointerChild(
373
- pointerChild(context.pointer, 'content'),
374
- JSON_MEDIA_TYPE
375
- ),
376
- 'schema'
377
- ),
378
- },
379
- schema: media.schema,
380
- };
381
- }
382
-
383
- async #resolveReferenceObject(value, context, label, seen = new Set()) {
384
- const object = requireRecord(
385
- value,
386
- locationOf(context.documentPath, context.pointer),
387
- label
388
- );
389
-
390
- if (object.$ref === undefined) {
391
- return { context, value: object };
392
- }
393
- const resolved = await this.#documents.resolveReference(
394
- object.$ref,
395
- context.documentPath
396
- );
397
- if (seen.has(resolved.canonicalKey)) {
398
- throw new Error(
399
- `Cyclic ${label.toLowerCase()} reference at ${resolved.canonicalKey}`
400
- );
401
- }
402
- seen.add(resolved.canonicalKey);
403
- return this.#resolveReferenceObject(
404
- resolved.value,
405
- { documentPath: resolved.documentPath, pointer: resolved.pointer },
406
- label,
407
- seen
408
- );
409
- }
410
- }
411
-
412
- export async function buildOpenApiModel(specPath) {
413
- return new ModelBuilder().build(specPath);
414
- }
415
-