🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@amritk/generate-examples

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@amritk/generate-examples - npm Package Compare versions

Comparing version
0.4.1
to
0.4.2
+2
-1
dist/generators/collect-example-imports.js

@@ -12,3 +12,4 @@ import { refToFilename } from '@amritk/helpers/ref-to-filename';

const typeName = refToName(ref, suffix);
return `import { type ${typeName}, ${typeName}Arbitrary } from './${filename}'`;
// `.js` extension so the emitted import resolves under Node ESM, not only Bun.
return `import { type ${typeName}, ${typeName}Arbitrary } from './${filename}.js'`;
};

@@ -15,0 +16,0 @@ /**

@@ -14,2 +14,3 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';

export declare const deriveExample: (schema: JSONSchema, rootSchema?: Record<string, unknown>, seen?: ReadonlySet<string>) => unknown;
export declare const mergeAllOf: (schema: JSONSchema) => JSONSchema;
/**

@@ -16,0 +17,0 @@ * Serializes a derived value into a TypeScript source expression. Handles the

@@ -475,3 +475,3 @@ import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';

]);
const mergeAllOf = (schema) => {
export const mergeAllOf = (schema) => {
const branches = hasAllOf(schema) ? schema.allOf : [];

@@ -478,0 +478,0 @@ const merged = {};

@@ -5,2 +5,6 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';

*
* A schema that references itself is wrapped in `fc.letrec` so the recursion is
* tied lazily — a plain `const NodeArbitrary = fc.record({ next: NodeArbitrary })`
* would throw a TDZ `ReferenceError` the moment the module is imported.
*
* @example

@@ -7,0 +11,0 @@ * ```typescript

import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
import { refToName } from '@amritk/helpers/ref-to-name';
import { hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasFormat, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasFormat, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
import { mergeAllOf } from './derive-example.js';
/**

@@ -9,2 +10,4 @@ * Derives the arbitrary const name from a type name.

const arbitraryName = (typeName) => `${typeName}Arbitrary`;
/** The letrec key used for a type's own (self-referential) arbitrary. */
const SELF_KEY = 'self';
/** Builds a `fc.string({ ... })` expression honouring format and length constraints. */

@@ -35,4 +38,16 @@ const stringExpr = (schema) => {

}
if (hasPattern(schema))
return `fc.stringMatching(/${schema.pattern}/)`;
if (hasPattern(schema)) {
// Build the regex via `new RegExp(<json-string>)` rather than inlining the
// pattern into a `/.../ ` literal: a pattern containing `/` (e.g. `^/api/v\d+$`)
// would otherwise close the literal early and emit invalid TypeScript.
const base = `fc.stringMatching(new RegExp(${JSON.stringify(schema.pattern)}))`;
// `stringMatching` takes no length bounds, so honour any min/maxLength with a
// filter instead of silently dropping them. Only emit it when a bound exists.
const checks = [];
if (hasMinLength(schema))
checks.push(`s.length >= ${schema.minLength}`);
if (hasMaxLength(schema))
checks.push(`s.length <= ${schema.maxLength}`);
return checks.length > 0 ? `${base}.filter((s) => ${checks.join(' && ')})` : base;
}
const opts = [];

@@ -48,10 +63,19 @@ if (hasMinLength(schema))

const opts = [];
// With both `minimum` and `exclusiveMinimum` present the effective lower bound
// is the tighter (larger) of the two, so combine them rather than letting one
// shadow the other via else-if.
const mins = [];
if (hasMinimum(schema))
opts.push(`min: ${schema.minimum}`);
else if (hasExclusiveMinimum(schema))
opts.push(`min: ${Number(schema.exclusiveMinimum) + 1}`);
mins.push(Number(schema.minimum));
if (hasExclusiveMinimum(schema))
mins.push(Number(schema.exclusiveMinimum) + 1);
if (mins.length > 0)
opts.push(`min: ${Math.max(...mins)}`);
const maxs = [];
if (hasMaximum(schema))
opts.push(`max: ${schema.maximum}`);
else if (hasExclusiveMaximum(schema))
opts.push(`max: ${Number(schema.exclusiveMaximum) - 1}`);
maxs.push(Number(schema.maximum));
if (hasExclusiveMaximum(schema))
maxs.push(Number(schema.exclusiveMaximum) - 1);
if (maxs.length > 0)
opts.push(`max: ${Math.min(...maxs)}`);
const base = opts.length > 0 ? `fc.integer({ ${opts.join(', ')} })` : 'fc.integer()';

@@ -74,5 +98,20 @@ return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base;

};
/** Builds a `fc.array(...)` / `fc.uniqueArray(...)` expression for an array schema. */
const arrayExpr = (schema, suffix) => {
const items = hasItems(schema) && isSchemaObject(schema.items) ? arbitraryExpr(schema.items, suffix) : 'fc.anything()';
/** Builds a `fc.array(...)` / `fc.uniqueArray(...)` / `fc.tuple(...)` expression for an array schema. */
const arrayExpr = (schema, ctx) => {
const raw = schema;
// A tuple is `prefixItems` (2020-12) or the draft-07 array-form `items`. Both
// describe one schema per position, so map them to `fc.tuple(...)`. Extra items
// beyond the prefix are unconstrained (or forbidden by `items: false`); the
// minimal tuple is schema-valid either way.
const prefixItems = raw['prefixItems'];
const tuple = Array.isArray(prefixItems)
? prefixItems
: Array.isArray(raw['items'])
? raw['items']
: undefined;
if (tuple) {
const exprs = tuple.map((item) => arbitraryExpr(item, ctx));
return `fc.tuple(${exprs.join(', ')})`;
}
const items = hasItems(schema) && isSchemaObject(schema.items) ? arbitraryExpr(schema.items, ctx) : 'fc.anything()';
const opts = [];

@@ -86,9 +125,16 @@ if (hasMinItems(schema))

};
/** Builds a `fc.record(...)` expression for an object schema. */
const objectExpr = (schema, suffix) => {
if (!hasProperties(schema))
return 'fc.object()';
/** Builds a `fc.record(...)` / `fc.dictionary(...)` expression for an object schema. */
const objectExpr = (schema, ctx) => {
// The `additionalProperties` value schema (when it constrains extra keys with a
// real subschema rather than the boolean true/false form).
const additional = hasAdditionalProperties(schema) ? schema.additionalProperties : false;
const additionalArb = isSchemaObject(additional) ? arbitraryExpr(additional, ctx) : undefined;
if (!hasProperties(schema)) {
// A map-style object (no declared properties) with a typed `additionalProperties`
// is a dictionary of that value type; otherwise fall back to a free-form object.
return additionalArb ? `fc.dictionary(fc.string(), ${additionalArb})` : 'fc.object()';
}
const required = new Set(hasRequired(schema) ? schema.required : []);
const keys = Object.keys(schema.properties);
const entries = Object.entries(schema.properties).map(([key, propSchema]) => `${JSON.stringify(key)}: ${arbitraryExpr(propSchema, suffix)}`);
const entries = Object.entries(schema.properties).map(([key, propSchema]) => `${JSON.stringify(key)}: ${arbitraryExpr(propSchema, ctx)}`);
if (entries.length === 0)

@@ -99,14 +145,20 @@ return 'fc.record({})';

// when at least one property is optional.
if (keys.every((key) => required.has(key)))
return `fc.record(${model})`;
const requiredKeys = [...required].map((key) => JSON.stringify(key)).join(', ');
return `fc.record(${model}, { requiredKeys: [${requiredKeys}] })`;
const record = keys.every((key) => required.has(key))
? `fc.record(${model})`
: `fc.record(${model}, { requiredKeys: [${[...required].map((key) => JSON.stringify(key)).join(', ')}] })`;
// When both declared properties and a typed `additionalProperties` are present,
// fold in a dictionary of extra keys so the value exercises the open-map part of
// the schema too. The declared keys win on collision (merged last).
if (additionalArb) {
return `fc.tuple(${record}, fc.dictionary(fc.string(), ${additionalArb})).map(([base, extra]) => ({ ...extra, ...base }))`;
}
return record;
};
/** Builds a `fc.oneof(...)` expression from a list of branch schemas. */
const oneofExpr = (branches, suffix) => {
const exprs = branches.map((branch) => arbitraryExpr(branch, suffix));
const oneofExpr = (branches, ctx) => {
const exprs = branches.map((branch) => arbitraryExpr(branch, ctx));
return `fc.oneof(${exprs.join(', ')})`;
};
/** Builds the fast-check expression for a single (non-union) JSON Schema type. */
const scalarExpr = (type, schema, suffix) => {
const scalarExpr = (type, schema, ctx) => {
switch (type) {

@@ -124,5 +176,5 @@ case 'string':

case 'array':
return arrayExpr(schema, suffix);
return arrayExpr(schema, ctx);
case 'object':
return objectExpr(schema, suffix);
return objectExpr(schema, ctx);
default:

@@ -134,10 +186,19 @@ return 'fc.anything()';

* Recursively builds the fast-check arbitrary expression for a schema node.
* `$ref`s resolve to the referenced file's exported arbitrary; everything else
* maps to the appropriate `fc.*` combinator.
* `$ref`s resolve to the referenced file's exported arbitrary; a self-`$ref`
* resolves to `tie('self')` so recursive schemas tie lazily via `fc.letrec`.
* Everything else maps to the appropriate `fc.*` combinator.
*/
const arbitraryExpr = (schema, suffix) => {
const arbitraryExpr = (schema, ctx) => {
if (!isSchemaObject(schema))
return 'fc.anything()';
if (hasRef(schema))
return arbitraryName(refToName(schema.$ref, suffix));
if (hasRef(schema)) {
const name = arbitraryName(refToName(schema.$ref, ctx.suffix));
// A reference back to the type being generated must be tied lazily; an eager
// identifier would touch a still-uninitialized const (TDZ) at import time.
if (name === ctx.selfArbName) {
ctx.usedTie.value = true;
return `tie(${JSON.stringify(SELF_KEY)})`;
}
return name;
}
if (hasConst(schema))

@@ -159,12 +220,17 @@ return `fc.constant(${JSON.stringify(schema.const)})`;

return 'fc.anything()';
// `allOf` must satisfy every branch at once. fast-check has no generic
// intersection combinator, so flatten the branches into one merged schema
// (tightest bounds, unioned required, merged properties) and generate from it.
if (hasAllOf(schema))
return arbitraryExpr(mergeAllOf(schema), ctx);
if (hasOneOf(schema))
return oneofExpr(schema.oneOf, suffix);
return oneofExpr(schema.oneOf, ctx);
if (hasAnyOf(schema))
return oneofExpr(schema.anyOf, suffix);
return oneofExpr(schema.anyOf, ctx);
if (hasType(schema))
return scalarExpr(schema.type, schema, suffix);
return scalarExpr(schema.type, schema, ctx);
// Multi-type schemas (`type: ['string', 'null']`) become a oneof over each
// member type; `hasType` only matches a single string `type`.
if (Array.isArray(schema.type)) {
const exprs = schema.type.map((type) => scalarExpr(type, schema, suffix));
const exprs = schema.type.map((type) => scalarExpr(type, schema, ctx));
return exprs.length === 1 ? exprs[0] : `fc.oneof(${exprs.join(', ')})`;

@@ -177,2 +243,6 @@ }

*
* A schema that references itself is wrapped in `fc.letrec` so the recursion is
* tied lazily — a plain `const NodeArbitrary = fc.record({ next: NodeArbitrary })`
* would throw a TDZ `ReferenceError` the moment the module is imported.
*
* @example

@@ -185,4 +255,9 @@ * ```typescript

export const generateArbitrary = (schema, typeName, suffix = '') => {
const expr = arbitraryExpr(schema, suffix);
return `export const ${arbitraryName(typeName)}: fc.Arbitrary<${typeName}> = ${expr}`;
const selfArbName = arbitraryName(typeName);
const ctx = { suffix, selfArbName, usedTie: { value: false } };
const expr = arbitraryExpr(schema, ctx);
if (ctx.usedTie.value) {
return `export const ${selfArbName}: fc.Arbitrary<${typeName}> = fc.letrec<{ ${SELF_KEY}: ${typeName} }>((tie) => ({\n ${SELF_KEY}: ${expr},\n})).${SELF_KEY}`;
}
return `export const ${selfArbName}: fc.Arbitrary<${typeName}> = ${expr}`;
};
{
"name": "@amritk/generate-examples",
"version": "0.4.1",
"version": "0.4.2",
"description": "Generate fast-check arbitraries and example values from JSON Schemas.",

@@ -44,4 +44,4 @@ "module": "./dist/index.js",

".": {
"default": "./dist/index.js",
"types": "./dist/index.d.ts"
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}

@@ -51,3 +51,3 @@ },

"json-schema-typed": "^8.0.1",
"@amritk/helpers": "0.10.1"
"@amritk/helpers": "0.10.2"
},

@@ -54,0 +54,0 @@ "devDependencies": {