openapi-contract-kit 0.0.3 → 0.0.5
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.
- package/CHANGELOG.md +8 -1
- package/README.md +2 -2
- package/dist/src/generator/emitMakers.js +15 -185
- package/dist/src/generator/validations/render-arrays.d.ts +1 -0
- package/dist/src/generator/validations/render-arrays.js +5 -0
- package/dist/src/generator/validations/render-numbers.d.ts +2 -0
- package/dist/src/generator/validations/render-numbers.js +24 -0
- package/dist/src/generator/validations/render-objects.d.ts +3 -0
- package/dist/src/generator/validations/render-objects.js +39 -0
- package/dist/src/generator/validations/render-primitives.d.ts +3 -0
- package/dist/src/generator/validations/render-primitives.js +36 -0
- package/dist/src/generator/validations/render-strings.d.ts +2 -0
- package/dist/src/generator/validations/render-strings.js +29 -0
- package/dist/src/generator/validations/render-unions.d.ts +2 -0
- package/dist/src/generator/validations/render-unions.js +19 -0
- package/dist/src/generator/validations/render-utils.d.ts +1 -0
- package/dist/src/generator/validations/render-utils.js +4 -0
- package/dist/src/generator/validations/types.d.ts +12 -0
- package/dist/src/generator/validations/types.js +1 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
## [0.0.5] - 2026-09-12
|
|
5
|
+
|
|
6
|
+
- Tighten generated pragmatic email-format validation and modularize primitive and constraint renderers.
|
|
7
|
+
|
|
8
|
+
## [0.0.4] - 2026-09-12
|
|
9
|
+
|
|
10
|
+
- Lowered the supported Node.js baseline to Node 24 and added CI coverage for Node 24 and 26.
|
|
4
11
|
|
|
5
12
|
## [0.0.3] - 2026-09-12
|
|
6
13
|
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ JSON-first OpenAPI 3.1 contract generation and runtime validation for TypeScript
|
|
|
19
19
|
pnpm add -D openapi-contract-kit
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Requires Node.js
|
|
22
|
+
Requires Node.js 24 or newer and pnpm 12.3.4.
|
|
23
23
|
|
|
24
24
|
## Quick start
|
|
25
25
|
|
|
@@ -63,7 +63,7 @@ if (!result.ok) {
|
|
|
63
63
|
|
|
64
64
|
Validation errors contain a path, keyword, and message. Successful validation returns the original input without mutating or cloning it.
|
|
65
65
|
|
|
66
|
-
The focused fixture and acceptance tests cover structural validation
|
|
66
|
+
The focused fixture and acceptance tests cover structural validation, primitive types, references, unions, additional-property behavior, and declared email format validation. String length, patterns, and numeric ranges remain available when declared in consumer schemas.
|
|
67
67
|
|
|
68
68
|
## Supported input
|
|
69
69
|
|
|
@@ -1,29 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return `Array.isArray(${value})`;
|
|
9
|
-
case 'boolean':
|
|
10
|
-
return `typeof ${value} === 'boolean'`;
|
|
11
|
-
case 'integer':
|
|
12
|
-
return `typeof ${value} === 'number' && Number.isFinite(${value}) && Number.isInteger(${value})`;
|
|
13
|
-
case 'null':
|
|
14
|
-
return `${value} === null`;
|
|
15
|
-
case 'number':
|
|
16
|
-
return `typeof ${value} === 'number' && Number.isFinite(${value})`;
|
|
17
|
-
case 'object':
|
|
18
|
-
return `typeof ${value} === 'object' && ${value} !== null && !Array.isArray(${value})`;
|
|
19
|
-
case 'string':
|
|
20
|
-
return `typeof ${value} === 'string'`;
|
|
21
|
-
default: {
|
|
22
|
-
const exhaustive = type;
|
|
23
|
-
throw new Error(`Unsupported normalised schema type "${exhaustive}"`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
import { renderArrayConstraints } from './validations/render-arrays.js';
|
|
2
|
+
import { renderNumberConstraints } from './validations/render-numbers.js';
|
|
3
|
+
import { renderObjectConstraints } from './validations/render-objects.js';
|
|
4
|
+
import { renderPrimitiveConstraints, typeExpression, } from './validations/render-primitives.js';
|
|
5
|
+
import { renderStringConstraints } from './validations/render-strings.js';
|
|
6
|
+
import { renderAllOf, renderUnionConstraint, } from './validations/render-unions.js';
|
|
7
|
+
import { indent } from './validations/render-utils.js';
|
|
27
8
|
class MakerRenderer {
|
|
28
9
|
#counter = 0;
|
|
29
10
|
#functions = [];
|
|
@@ -155,168 +136,17 @@ export function make${name}(input: unknown): Result<${name}> {
|
|
|
155
136
|
'return false;',
|
|
156
137
|
]), '}');
|
|
157
138
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
.join(', ');
|
|
167
|
-
lines.push(`if (![${values}].some((candidate) => Object.is(candidate, value))) {`, ...indent([
|
|
168
|
-
`errors.push({ path, keyword: 'enum', message: 'Expected a documented enum value' });`,
|
|
169
|
-
]), '}');
|
|
170
|
-
}
|
|
171
|
-
for (const child of children.allOf) {
|
|
172
|
-
lines.push(`${child}(value, path, errors);`);
|
|
173
|
-
}
|
|
174
|
-
this.#renderUnionConstraint(lines, 'anyOf', children.anyOf, false);
|
|
175
|
-
this.#renderUnionConstraint(lines, 'oneOf', children.oneOf, true);
|
|
176
|
-
this.#renderStringConstraints(lines, schema);
|
|
177
|
-
this.#renderNumberConstraints(lines, schema);
|
|
178
|
-
this.#renderArrayConstraints(lines, children.items);
|
|
179
|
-
this.#renderObjectConstraints(lines, schema, children);
|
|
139
|
+
renderPrimitiveConstraints(lines, schema.constValue, schema.enumValues);
|
|
140
|
+
renderAllOf(lines, children.allOf);
|
|
141
|
+
renderUnionConstraint(lines, 'anyOf', children.anyOf, false);
|
|
142
|
+
renderUnionConstraint(lines, 'oneOf', children.oneOf, true);
|
|
143
|
+
renderStringConstraints(lines, schema);
|
|
144
|
+
renderNumberConstraints(lines, schema);
|
|
145
|
+
renderArrayConstraints(lines, children.items);
|
|
146
|
+
renderObjectConstraints(lines, schema, children);
|
|
180
147
|
lines.push('return errors.length === errorCount;');
|
|
181
148
|
return lines;
|
|
182
149
|
}
|
|
183
|
-
#renderUnionConstraint(lines, keyword, branches, isExclusive) {
|
|
184
|
-
if (branches.length === 0) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const variable = `${keyword}Matches`;
|
|
188
|
-
lines.push(`let ${variable} = 0;`);
|
|
189
|
-
for (const [index, branch] of branches.entries()) {
|
|
190
|
-
const errorsName = `${keyword}Errors${index}`;
|
|
191
|
-
lines.push(`const ${errorsName}: ValidationIssue[] = [];`, `if (${branch}(value, path, ${errorsName})) {`, ...indent([`${variable} += 1;`]), '}');
|
|
192
|
-
}
|
|
193
|
-
const invalidExpression = isExclusive
|
|
194
|
-
? `${variable} !== 1`
|
|
195
|
-
: `${variable} === 0`;
|
|
196
|
-
const expectation = isExclusive ? 'exactly one' : 'at least one';
|
|
197
|
-
lines.push(`if (${invalidExpression}) {`, ...indent([
|
|
198
|
-
`errors.push({ path, keyword: '${keyword}', message: 'Expected ${expectation} matching branch' });`,
|
|
199
|
-
]), '}');
|
|
200
|
-
}
|
|
201
|
-
#renderStringConstraints(lines, schema) {
|
|
202
|
-
if (schema.format === null &&
|
|
203
|
-
schema.maxLength === null &&
|
|
204
|
-
schema.minLength === null &&
|
|
205
|
-
schema.pattern === null) {
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
lines.push(`if (typeof value === 'string') {`);
|
|
209
|
-
const checks = [];
|
|
210
|
-
if (schema.minLength !== null) {
|
|
211
|
-
checks.push(`if (Array.from(value).length < ${schema.minLength}) {`, ...indent([
|
|
212
|
-
`errors.push({ path, keyword: 'minLength', message: 'String is shorter than ${schema.minLength} characters' });`,
|
|
213
|
-
]), '}');
|
|
214
|
-
}
|
|
215
|
-
if (schema.maxLength !== null) {
|
|
216
|
-
checks.push(`if (Array.from(value).length > ${schema.maxLength}) {`, ...indent([
|
|
217
|
-
`errors.push({ path, keyword: 'maxLength', message: 'String is longer than ${schema.maxLength} characters' });`,
|
|
218
|
-
]), '}');
|
|
219
|
-
}
|
|
220
|
-
if (schema.pattern !== null) {
|
|
221
|
-
checks.push(`if (!new RegExp(${JSON.stringify(schema.pattern)}, 'u').test(value)) {`, ...indent([
|
|
222
|
-
`errors.push({ path, keyword: 'pattern', message: 'String does not match the documented pattern' });`,
|
|
223
|
-
]), '}');
|
|
224
|
-
}
|
|
225
|
-
if (schema.format === 'email') {
|
|
226
|
-
checks.push(`if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/u.test(value)) {`, ...indent([
|
|
227
|
-
`errors.push({ path, keyword: 'format', message: 'Expected email format' });`,
|
|
228
|
-
]), '}');
|
|
229
|
-
}
|
|
230
|
-
lines.push(...indent(checks), '}');
|
|
231
|
-
}
|
|
232
|
-
#renderNumberConstraints(lines, schema) {
|
|
233
|
-
if (schema.exclusiveMaximum === null &&
|
|
234
|
-
schema.exclusiveMinimum === null &&
|
|
235
|
-
schema.maximum === null &&
|
|
236
|
-
schema.minimum === null) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
lines.push(`if (typeof value === 'number' && Number.isFinite(value)) {`);
|
|
240
|
-
const checks = [];
|
|
241
|
-
const addCheck = (expression, keyword, message) => {
|
|
242
|
-
checks.push(`if (${expression}) {`, ...indent([
|
|
243
|
-
`errors.push({ path, keyword: '${keyword}', message: ${JSON.stringify(message)} });`,
|
|
244
|
-
]), '}');
|
|
245
|
-
};
|
|
246
|
-
if (schema.minimum !== null) {
|
|
247
|
-
addCheck(`value < ${schema.minimum}`, 'minimum', `Number must be at least ${schema.minimum}`);
|
|
248
|
-
}
|
|
249
|
-
if (schema.maximum !== null) {
|
|
250
|
-
addCheck(`value > ${schema.maximum}`, 'maximum', `Number must be at most ${schema.maximum}`);
|
|
251
|
-
}
|
|
252
|
-
if (schema.exclusiveMinimum !== null) {
|
|
253
|
-
addCheck(`value <= ${schema.exclusiveMinimum}`, 'exclusiveMinimum', `Number must be greater than ${schema.exclusiveMinimum}`);
|
|
254
|
-
}
|
|
255
|
-
if (schema.exclusiveMaximum !== null) {
|
|
256
|
-
addCheck(`value >= ${schema.exclusiveMaximum}`, 'exclusiveMaximum', `Number must be less than ${schema.exclusiveMaximum}`);
|
|
257
|
-
}
|
|
258
|
-
lines.push(...indent(checks), '}');
|
|
259
|
-
}
|
|
260
|
-
#renderArrayConstraints(lines, itemValidator) {
|
|
261
|
-
if (itemValidator === null) {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
lines.push('if (Array.isArray(value)) {', ...indent([
|
|
265
|
-
'for (let index = 0; index < value.length; index += 1) {',
|
|
266
|
-
...indent(['const item = value[index];']),
|
|
267
|
-
...indent([`${itemValidator}(item, [...path, index], errors);`]),
|
|
268
|
-
'}',
|
|
269
|
-
]), '}');
|
|
270
|
-
}
|
|
271
|
-
#renderObjectConstraints(lines, schema, children) {
|
|
272
|
-
if (children.properties.length === 0 &&
|
|
273
|
-
children.additionalProperties === null &&
|
|
274
|
-
schema.additionalProperties !== false) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
lines.push(`if (typeof value === 'object' && value !== null && !Array.isArray(value)) {`);
|
|
278
|
-
const checks = [];
|
|
279
|
-
for (const property of children.properties) {
|
|
280
|
-
const hasProperty = `Object.prototype.hasOwnProperty.call(value, ${JSON.stringify(property.name)})`;
|
|
281
|
-
const propertyPath = `[...path, ${JSON.stringify(property.name)}]`;
|
|
282
|
-
if (property.required) {
|
|
283
|
-
checks.push(`if (!${hasProperty}) {`, ...indent([
|
|
284
|
-
`errors.push({ path: ${propertyPath}, keyword: 'required', message: 'Required property is missing' });`,
|
|
285
|
-
]), '} else {', ...indent([
|
|
286
|
-
`${property.functionName}(Reflect.get(value, ${JSON.stringify(property.name)}), ${propertyPath}, errors);`,
|
|
287
|
-
]), '}');
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
checks.push(`if (${hasProperty}) {`, ...indent([
|
|
291
|
-
`${property.functionName}(Reflect.get(value, ${JSON.stringify(property.name)}), ${propertyPath}, errors);`,
|
|
292
|
-
]), '}');
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (schema.additionalProperties === false ||
|
|
296
|
-
children.additionalProperties !== null) {
|
|
297
|
-
const propertyNames = children.properties.map(({ name }) => name);
|
|
298
|
-
checks.push('for (const key of Object.keys(value)) {');
|
|
299
|
-
const isAdditionalProperty = propertyNames.length === 0
|
|
300
|
-
? 'true'
|
|
301
|
-
: `![${propertyNames
|
|
302
|
-
.map((name) => JSON.stringify(name))
|
|
303
|
-
.join(', ')}].includes(key)`;
|
|
304
|
-
const additionalChecks = [`if (${isAdditionalProperty}) {`];
|
|
305
|
-
if (schema.additionalProperties === false) {
|
|
306
|
-
additionalChecks.push(...indent([
|
|
307
|
-
`errors.push({ path: [...path, key], keyword: 'additionalProperties', message: 'Unknown property is not allowed' });`,
|
|
308
|
-
]));
|
|
309
|
-
}
|
|
310
|
-
else if (children.additionalProperties !== null) {
|
|
311
|
-
additionalChecks.push(...indent([
|
|
312
|
-
`${children.additionalProperties}(Reflect.get(value, key), [...path, key], errors);`,
|
|
313
|
-
]));
|
|
314
|
-
}
|
|
315
|
-
additionalChecks.push('}');
|
|
316
|
-
checks.push(...indent(additionalChecks), '}');
|
|
317
|
-
}
|
|
318
|
-
lines.push(...indent(checks), '}');
|
|
319
|
-
}
|
|
320
150
|
}
|
|
321
151
|
export function emitSchemaMakers(model, config) {
|
|
322
152
|
const files = new Map();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderArrayConstraints(lines: string[], itemValidator: string | null): void;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export function renderArrayConstraints(lines, itemValidator) {
|
|
2
|
+
if (itemValidator === null)
|
|
3
|
+
return;
|
|
4
|
+
lines.push('if (Array.isArray(value)) {', ' for (let index = 0; index < value.length; index += 1) {', ' const item = value[index];', ` ${itemValidator}(item, [...path, index], errors);`, ' }', '}');
|
|
5
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { indent } from './render-utils.js';
|
|
2
|
+
export function renderNumberConstraints(lines, schema) {
|
|
3
|
+
if (schema.exclusiveMaximum === null &&
|
|
4
|
+
schema.exclusiveMinimum === null &&
|
|
5
|
+
schema.maximum === null &&
|
|
6
|
+
schema.minimum === null)
|
|
7
|
+
return;
|
|
8
|
+
lines.push("if (typeof value === 'number' && Number.isFinite(value)) {");
|
|
9
|
+
const checks = [];
|
|
10
|
+
const addCheck = (expression, keyword, message) => {
|
|
11
|
+
checks.push(`if (${expression}) {`, ...indent([
|
|
12
|
+
`errors.push({ path, keyword: '${keyword}', message: ${JSON.stringify(message)} });`,
|
|
13
|
+
]), '}');
|
|
14
|
+
};
|
|
15
|
+
if (schema.minimum !== null)
|
|
16
|
+
addCheck(`value < ${schema.minimum}`, 'minimum', `Number must be at least ${schema.minimum}`);
|
|
17
|
+
if (schema.maximum !== null)
|
|
18
|
+
addCheck(`value > ${schema.maximum}`, 'maximum', `Number must be at most ${schema.maximum}`);
|
|
19
|
+
if (schema.exclusiveMinimum !== null)
|
|
20
|
+
addCheck(`value <= ${schema.exclusiveMinimum}`, 'exclusiveMinimum', `Number must be greater than ${schema.exclusiveMinimum}`);
|
|
21
|
+
if (schema.exclusiveMaximum !== null)
|
|
22
|
+
addCheck(`value >= ${schema.exclusiveMaximum}`, 'exclusiveMaximum', `Number must be less than ${schema.exclusiveMaximum}`);
|
|
23
|
+
lines.push(...indent(checks), '}');
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { indent } from './render-utils.js';
|
|
2
|
+
export function renderObjectConstraints(lines, schema, children) {
|
|
3
|
+
if (children.properties.length === 0 &&
|
|
4
|
+
children.additionalProperties === null &&
|
|
5
|
+
schema.additionalProperties !== false)
|
|
6
|
+
return;
|
|
7
|
+
lines.push("if (typeof value === 'object' && value !== null && !Array.isArray(value)) {");
|
|
8
|
+
const checks = [];
|
|
9
|
+
for (const property of children.properties) {
|
|
10
|
+
const hasProperty = `Object.prototype.hasOwnProperty.call(value, ${JSON.stringify(property.name)})`;
|
|
11
|
+
const propertyPath = `[...path, ${JSON.stringify(property.name)}]`;
|
|
12
|
+
const validation = `${property.functionName}(Reflect.get(value, ${JSON.stringify(property.name)}), ${propertyPath}, errors);`;
|
|
13
|
+
if (property.required)
|
|
14
|
+
checks.push(`if (!${hasProperty}) {`, ...indent([
|
|
15
|
+
`errors.push({ path: ${propertyPath}, keyword: 'required', message: 'Required property is missing' });`,
|
|
16
|
+
]), '} else {', ...indent([validation]), '}');
|
|
17
|
+
else
|
|
18
|
+
checks.push(`if (${hasProperty}) {`, ...indent([validation]), '}');
|
|
19
|
+
}
|
|
20
|
+
if (schema.additionalProperties === false ||
|
|
21
|
+
children.additionalProperties !== null) {
|
|
22
|
+
const propertyNames = children.properties.map(({ name }) => name);
|
|
23
|
+
const isAdditional = propertyNames.length === 0
|
|
24
|
+
? 'true'
|
|
25
|
+
: `![${propertyNames.map((name) => JSON.stringify(name)).join(', ')}].includes(key)`;
|
|
26
|
+
const additional = [`if (${isAdditional}) {`];
|
|
27
|
+
if (schema.additionalProperties === false)
|
|
28
|
+
additional.push(...indent([
|
|
29
|
+
`errors.push({ path: [...path, key], keyword: 'additionalProperties', message: 'Unknown property is not allowed' });`,
|
|
30
|
+
]));
|
|
31
|
+
else if (children.additionalProperties !== null)
|
|
32
|
+
additional.push(...indent([
|
|
33
|
+
`${children.additionalProperties}(Reflect.get(value, key), [...path, key], errors);`,
|
|
34
|
+
]));
|
|
35
|
+
additional.push('}');
|
|
36
|
+
checks.push('for (const key of Object.keys(value)) {', ...indent(additional), '}');
|
|
37
|
+
}
|
|
38
|
+
lines.push(...indent(checks), '}');
|
|
39
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { JsonPrimitive, SchemaType } from '../types.js';
|
|
2
|
+
export declare function typeExpression(type: SchemaType, value?: string): string;
|
|
3
|
+
export declare function renderPrimitiveConstraints(lines: string[], constValue: JsonPrimitive | undefined, enumValues: readonly JsonPrimitive[] | null): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { indent } from './render-utils.js';
|
|
2
|
+
export function typeExpression(type, value = 'value') {
|
|
3
|
+
switch (type) {
|
|
4
|
+
case 'array':
|
|
5
|
+
return `Array.isArray(${value})`;
|
|
6
|
+
case 'boolean':
|
|
7
|
+
return `typeof ${value} === 'boolean'`;
|
|
8
|
+
case 'integer':
|
|
9
|
+
return `typeof ${value} === 'number' && Number.isFinite(${value}) && Number.isInteger(${value})`;
|
|
10
|
+
case 'null':
|
|
11
|
+
return `${value} === null`;
|
|
12
|
+
case 'number':
|
|
13
|
+
return `typeof ${value} === 'number' && Number.isFinite(${value})`;
|
|
14
|
+
case 'object':
|
|
15
|
+
return `typeof ${value} === 'object' && ${value} !== null && !Array.isArray(${value})`;
|
|
16
|
+
case 'string':
|
|
17
|
+
return `typeof ${value} === 'string'`;
|
|
18
|
+
default: {
|
|
19
|
+
const exhaustive = type;
|
|
20
|
+
throw new Error(`Unsupported normalised schema type "${exhaustive}"`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function renderPrimitiveConstraints(lines, constValue, enumValues) {
|
|
25
|
+
if (constValue !== undefined) {
|
|
26
|
+
lines.push(`if (!Object.is(value, ${JSON.stringify(constValue)})) {`, ...indent([
|
|
27
|
+
`errors.push({ path, keyword: 'const', message: 'Expected the documented constant value' });`,
|
|
28
|
+
]), '}');
|
|
29
|
+
}
|
|
30
|
+
if (enumValues !== null) {
|
|
31
|
+
const values = enumValues.map((value) => JSON.stringify(value)).join(', ');
|
|
32
|
+
lines.push(`if (![${values}].some((candidate) => Object.is(candidate, value))) {`, ...indent([
|
|
33
|
+
`errors.push({ path, keyword: 'enum', message: 'Expected a documented enum value' });`,
|
|
34
|
+
]), '}');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { indent } from './render-utils.js';
|
|
2
|
+
const EMAIL_FORMAT_PATTERN = "^[A-Za-z0-9!#$%&'*+\\/=?^_`{|}~-]+(?:\\.[A-Za-z0-9!#$%&'*+\\/=?^_`{|}~-]+)*@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\\.[A-Za-z]{2,63}$";
|
|
3
|
+
export function renderStringConstraints(lines, schema) {
|
|
4
|
+
if (schema.format === null &&
|
|
5
|
+
schema.maxLength === null &&
|
|
6
|
+
schema.minLength === null &&
|
|
7
|
+
schema.pattern === null)
|
|
8
|
+
return;
|
|
9
|
+
lines.push("if (typeof value === 'string') {");
|
|
10
|
+
const checks = [];
|
|
11
|
+
if (schema.minLength !== null)
|
|
12
|
+
checks.push(`if (Array.from(value).length < ${schema.minLength}) {`, ...indent([
|
|
13
|
+
`errors.push({ path, keyword: 'minLength', message: 'String is shorter than ${schema.minLength} characters' });`,
|
|
14
|
+
]), '}');
|
|
15
|
+
if (schema.maxLength !== null)
|
|
16
|
+
checks.push(`if (Array.from(value).length > ${schema.maxLength}) {`, ...indent([
|
|
17
|
+
`errors.push({ path, keyword: 'maxLength', message: 'String is longer than ${schema.maxLength} characters' });`,
|
|
18
|
+
]), '}');
|
|
19
|
+
if (schema.pattern !== null)
|
|
20
|
+
checks.push(`if (!new RegExp(${JSON.stringify(schema.pattern)}, 'u').test(value)) {`, ...indent([
|
|
21
|
+
`errors.push({ path, keyword: 'pattern', message: 'String does not match the documented pattern' });`,
|
|
22
|
+
]), '}');
|
|
23
|
+
if (schema.format === 'email') {
|
|
24
|
+
checks.push(`if (!new RegExp(${JSON.stringify(EMAIL_FORMAT_PATTERN)}).test(value)) {`, ...indent([
|
|
25
|
+
`errors.push({ path, keyword: 'format', message: 'Expected email format' });`,
|
|
26
|
+
]), '}');
|
|
27
|
+
}
|
|
28
|
+
lines.push(...indent(checks), '}');
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function renderAllOf(lines, children) {
|
|
2
|
+
for (const child of children)
|
|
3
|
+
lines.push(`${child}(value, path, errors);`);
|
|
4
|
+
}
|
|
5
|
+
export function renderUnionConstraint(lines, keyword, branches, isExclusive) {
|
|
6
|
+
if (branches.length === 0)
|
|
7
|
+
return;
|
|
8
|
+
const variable = `${keyword}Matches`;
|
|
9
|
+
lines.push(`let ${variable} = 0;`);
|
|
10
|
+
for (const [index, branch] of branches.entries()) {
|
|
11
|
+
const errorsName = `${keyword}Errors${index}`;
|
|
12
|
+
lines.push(`const ${errorsName}: ValidationIssue[] = [];`, `if (${branch}(value, path, ${errorsName})) {`, ` ${variable} += 1;`, '}');
|
|
13
|
+
}
|
|
14
|
+
const invalidExpression = isExclusive
|
|
15
|
+
? `${variable} !== 1`
|
|
16
|
+
: `${variable} === 0`;
|
|
17
|
+
const expectation = isExclusive ? 'exactly one' : 'at least one';
|
|
18
|
+
lines.push(`if (${invalidExpression}) {`, ` errors.push({ path, keyword: '${keyword}', message: 'Expected ${expectation} matching branch' });`, '}');
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function indent(lines: readonly string[], spaces?: number): string[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SchemaProperty } from '../types.js';
|
|
2
|
+
export type ChildFunctions = {
|
|
3
|
+
readonly additionalProperties: string | null;
|
|
4
|
+
readonly allOf: readonly string[];
|
|
5
|
+
readonly anyOf: readonly string[];
|
|
6
|
+
readonly items: string | null;
|
|
7
|
+
readonly oneOf: readonly string[];
|
|
8
|
+
readonly properties: readonly (SchemaProperty & {
|
|
9
|
+
readonly functionName: string;
|
|
10
|
+
})[];
|
|
11
|
+
readonly reference: string | null;
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openapi-contract-kit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Generate type-safe TypeScript contracts and runtime validators from JSON OpenAPI 3.1 documents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=24.0.0"
|
|
9
9
|
},
|
|
10
10
|
"main": "./dist/src/index.js",
|
|
11
11
|
"types": "./dist/src/index.d.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
],
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@eslint/js": "^10.0.1",
|
|
35
|
-
"@types/node": "^
|
|
35
|
+
"@types/node": "^24.0.0",
|
|
36
36
|
"eslint": "^10.10.0",
|
|
37
37
|
"prettier": "^3.9.6",
|
|
38
38
|
"typescript": "^5.9.3",
|