@amritk/generate-examples 0.0.0 → 0.2.0

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.
@@ -0,0 +1,34 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Represents a generated TypeScript file with its filename and content.
4
+ */
5
+ export type GeneratedFile = {
6
+ filename: string;
7
+ content: string;
8
+ };
9
+ /**
10
+ * Builds all TypeScript example files from a JSON Schema by traversing all
11
+ * `$ref` / `$dynamicRef` references recursively (via the shared
12
+ * `@amritk/helpers/walk-ref-graph` walker).
13
+ *
14
+ * Each generated file exports:
15
+ * - A TypeScript type definition
16
+ * - A `fast-check` arbitrary (`FooArbitrary`) that produces schema-valid values
17
+ * - A concrete example value (`fooExample`)
18
+ *
19
+ * An `index.ts` re-exports everything. The generated output imports `fast-check`,
20
+ * which consumers must install as a (dev) dependency.
21
+ *
22
+ * @param rootSchema - The root JSON Schema to build from
23
+ * @param rootTypeName - The name for the root type (e.g. "Document")
24
+ * @param typeSuffix - Suffix appended to every `$ref`-derived name (default `''`)
25
+ * @returns An array of generated TypeScript files
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const files = await buildExampleSchema(schema, 'Document')
30
+ * // files → [{ filename: 'document.ts', content: '...' }, { filename: 'index.ts', ... }]
31
+ * ```
32
+ */
33
+ export declare const buildExampleSchema: (rootSchema: JSONSchema, rootTypeName: string, typeSuffix?: string) => Promise<GeneratedFile[]>;
34
+ //# sourceMappingURL=build-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-schema.d.ts","sourceRoot":"","sources":["../../src/generators/build-schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAIjE;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,kBAAkB,eACjB,UAAU,gBACR,MAAM,0BAEnB,OAAO,CAAC,aAAa,EAAE,CAmBzB,CAAA"}
@@ -0,0 +1,44 @@
1
+ import { generateIndexBarrel } from '@amritk/helpers/generate-index-barrel';
2
+ import { walkRefGraph } from '@amritk/helpers/walk-ref-graph';
3
+ import { generateExampleFile } from './generate-files.js';
4
+ /**
5
+ * Builds all TypeScript example files from a JSON Schema by traversing all
6
+ * `$ref` / `$dynamicRef` references recursively (via the shared
7
+ * `@amritk/helpers/walk-ref-graph` walker).
8
+ *
9
+ * Each generated file exports:
10
+ * - A TypeScript type definition
11
+ * - A `fast-check` arbitrary (`FooArbitrary`) that produces schema-valid values
12
+ * - A concrete example value (`fooExample`)
13
+ *
14
+ * An `index.ts` re-exports everything. The generated output imports `fast-check`,
15
+ * which consumers must install as a (dev) dependency.
16
+ *
17
+ * @param rootSchema - The root JSON Schema to build from
18
+ * @param rootTypeName - The name for the root type (e.g. "Document")
19
+ * @param typeSuffix - Suffix appended to every `$ref`-derived name (default `''`)
20
+ * @returns An array of generated TypeScript files
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const files = await buildExampleSchema(schema, 'Document')
25
+ * // files → [{ filename: 'document.ts', content: '...' }, { filename: 'index.ts', ... }]
26
+ * ```
27
+ */
28
+ export const buildExampleSchema = async (rootSchema, rootTypeName, typeSuffix = '') => {
29
+ const files = [];
30
+ walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
31
+ // `index` is reserved for the barrel below, so never let a definition of
32
+ // that name overwrite it.
33
+ if (node.filename === 'index')
34
+ return;
35
+ const content = generateExampleFile(node.schema, node.typeName, {
36
+ rootSchema: node.rootSchema,
37
+ typeSuffix,
38
+ ...(node.ref !== undefined ? { selfRef: node.ref } : {}),
39
+ });
40
+ files.push({ filename: `${node.filename}.ts`, content });
41
+ });
42
+ files.push({ filename: 'index.ts', content: generateIndexBarrel(files) });
43
+ return files;
44
+ };
@@ -0,0 +1,35 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Options for controlling how example imports are collected.
4
+ */
5
+ type CollectExampleImportsOptions = {
6
+ /**
7
+ * The $ref path of the schema being generated (e.g. `#/$defs/address`).
8
+ * Prevents a file from importing itself.
9
+ */
10
+ readonly selfRef?: string | undefined;
11
+ /**
12
+ * The root schema document. URI refs that cannot be resolved within it
13
+ * are excluded from the import list (they were never generated as files).
14
+ */
15
+ readonly rootSchema?: Record<string, unknown> | undefined;
16
+ /**
17
+ * Suffix appended to every type/arbitrary name derived from a `$ref`. Must
18
+ * match the suffix used when generating the referenced files. Defaults to `''`.
19
+ */
20
+ readonly typeSuffix?: string;
21
+ };
22
+ /**
23
+ * Collects import statements for all $ref dependencies of a schema. Each import
24
+ * brings in both the generated TypeScript type and the arbitrary for that ref.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const schema = { properties: { address: { $ref: '#/$defs/address' } } }
29
+ * collectExampleImports(schema)
30
+ * // ["import { type Address, AddressArbitrary } from './address'"]
31
+ * ```
32
+ */
33
+ export declare const collectExampleImports: (schema: JSONSchema, options?: CollectExampleImportsOptions) => string[];
34
+ export {};
35
+ //# sourceMappingURL=collect-example-imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-example-imports.d.ts","sourceRoot":"","sources":["../../src/generators/collect-example-imports.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAEjE;;GAEG;AACH,KAAK,4BAA4B,GAAG;IAClC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACrC;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;IACzD;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AA0DD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,WAAY,UAAU,YAAY,4BAA4B,KAAG,MAAM,EA0BxG,CAAA"}
@@ -0,0 +1,88 @@
1
+ import { refToFilename } from '@amritk/helpers/ref-to-filename';
2
+ import { refToName } from '@amritk/helpers/ref-to-name';
3
+ import { resolveRef } from '@amritk/helpers/resolve-ref';
4
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, hasRef } from '@amritk/helpers/schema-guards';
5
+ /**
6
+ * Generates an import statement for a single $ref, importing both the generated
7
+ * type and its arbitrary from the ref's generated file.
8
+ */
9
+ const buildImport = (ref, suffix) => {
10
+ const filename = refToFilename(ref);
11
+ const typeName = refToName(ref, suffix);
12
+ return `import { type ${typeName}, ${typeName}Arbitrary } from './${filename}'`;
13
+ };
14
+ /**
15
+ * Walks one level of the schema and yields all direct $ref strings that should
16
+ * become imports: properties, additionalProperties, items, and union branches.
17
+ */
18
+ const collectDirectRefs = (schema) => {
19
+ if (typeof schema === 'boolean' || schema === null)
20
+ return [];
21
+ const refs = [];
22
+ if (hasRef(schema)) {
23
+ refs.push(schema.$ref);
24
+ return refs;
25
+ }
26
+ const propSchemas = 'properties' in schema && typeof schema.properties === 'object' && schema.properties !== null
27
+ ? Object.values(schema.properties)
28
+ : [];
29
+ for (const prop of propSchemas) {
30
+ if (hasRef(prop))
31
+ refs.push(prop.$ref);
32
+ if (hasItems(prop) && hasRef(prop.items))
33
+ refs.push(prop.items.$ref);
34
+ if (hasAdditionalProperties(prop) && hasRef(prop.additionalProperties)) {
35
+ refs.push(prop.additionalProperties.$ref);
36
+ }
37
+ }
38
+ if (hasItems(schema) && hasRef(schema.items)) {
39
+ refs.push(schema.items.$ref);
40
+ }
41
+ if (hasAdditionalProperties(schema) && hasRef(schema.additionalProperties)) {
42
+ refs.push(schema.additionalProperties.$ref);
43
+ }
44
+ for (const branch of [
45
+ ...(hasOneOf(schema) ? schema.oneOf : []),
46
+ ...(hasAnyOf(schema) ? schema.anyOf : []),
47
+ ...(hasAllOf(schema) ? schema.allOf : []),
48
+ ]) {
49
+ if (hasRef(branch))
50
+ refs.push(branch.$ref);
51
+ }
52
+ return refs;
53
+ };
54
+ /**
55
+ * Collects import statements for all $ref dependencies of a schema. Each import
56
+ * brings in both the generated TypeScript type and the arbitrary for that ref.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const schema = { properties: { address: { $ref: '#/$defs/address' } } }
61
+ * collectExampleImports(schema)
62
+ * // ["import { type Address, AddressArbitrary } from './address'"]
63
+ * ```
64
+ */
65
+ export const collectExampleImports = (schema, options) => {
66
+ const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null;
67
+ const rootSchema = options?.rootSchema;
68
+ const typeSuffix = options?.typeSuffix ?? '';
69
+ const refs = collectDirectRefs(schema);
70
+ const seen = new Set();
71
+ const imports = [];
72
+ for (const ref of refs) {
73
+ const filename = refToFilename(ref);
74
+ if (seen.has(filename))
75
+ continue;
76
+ if (selfFilename && filename === selfFilename)
77
+ continue;
78
+ // Skip refs that don't resolve in this schema (external / never generated)
79
+ if (rootSchema) {
80
+ const resolved = resolveRef(ref, rootSchema);
81
+ if (!resolved)
82
+ continue;
83
+ }
84
+ seen.add(filename);
85
+ imports.push(buildImport(ref, typeSuffix));
86
+ }
87
+ return imports;
88
+ };
@@ -0,0 +1,30 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Derives a single concrete, schema-valid value from a JSON Schema.
4
+ *
5
+ * Prefers explicit hints in this order: `const`, `examples[0]`, `default`,
6
+ * `enum[0]`; otherwise produces a canonical value for the declared type.
7
+ * `$ref`s are resolved and inlined by value; recursive refs short-circuit to
8
+ * `null` (tracked via `seen`).
9
+ *
10
+ * Note: values constrained only by `pattern` are not guaranteed to match the
11
+ * pattern — use the generated arbitrary when pattern fidelity matters.
12
+ */
13
+ export declare const deriveExample: (schema: JSONSchema, rootSchema?: Record<string, unknown>, seen?: ReadonlySet<string>) => unknown;
14
+ /**
15
+ * Serializes a derived value into a TypeScript source expression. Handles the
16
+ * non-JSON values `deriveExample` can produce (`Date`, `bigint`) in addition to
17
+ * plain JSON.
18
+ */
19
+ export declare const serializeValue: (value: unknown) => string;
20
+ /**
21
+ * Generates an exported const holding a concrete, schema-valid example value.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * generateExampleConst({ type: 'object', properties: { name: { type: 'string' } } }, 'Info')
26
+ * // export const infoExample: Info = { "name": "string" }
27
+ * ```
28
+ */
29
+ export declare const generateExampleConst: (schema: JSONSchema, typeName: string, rootSchema?: Record<string, unknown>) => string;
30
+ //# sourceMappingURL=derive-example.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive-example.d.ts","sourceRoot":"","sources":["../../src/generators/derive-example.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAgCjE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,WAChB,UAAU,eACL,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,SAC9B,WAAW,CAAC,MAAM,CAAC,KACxB,OAuDF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,UAAW,OAAO,KAAG,MAW/C,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,WACvB,UAAU,YACR,MAAM,eACH,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACnC,MAGF,CAAA"}
@@ -0,0 +1,137 @@
1
+ import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
2
+ import { resolveRef } from '@amritk/helpers/resolve-ref';
3
+ import { hasAnyOf, hasConst, hasDefault, hasEnum, hasExamples, hasFormat, hasItems, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasOneOf, hasProperties, hasRef, hasType, isSchemaObject, } from '@amritk/helpers/schema-guards';
4
+ /** Lowercases the first character of a name. e.g. "User" → "user" */
5
+ const lowerFirst = (name) => name.charAt(0).toLowerCase() + name.slice(1);
6
+ /** Derives the example const name from a type name. e.g. "User" → "userExample" */
7
+ const exampleName = (typeName) => `${lowerFirst(typeName)}Example`;
8
+ /** Returns a representative string honouring `format` and length constraints. */
9
+ const exampleString = (schema) => {
10
+ if (hasFormat(schema)) {
11
+ switch (schema.format) {
12
+ case 'email':
13
+ return 'user@example.com';
14
+ case 'uuid':
15
+ return '00000000-0000-0000-0000-000000000000';
16
+ case 'uri':
17
+ case 'url':
18
+ return 'https://example.com';
19
+ case 'date-time':
20
+ return '1970-01-01T00:00:00.000Z';
21
+ case 'date':
22
+ return '1970-01-01';
23
+ }
24
+ }
25
+ let value = 'string';
26
+ if (hasMinLength(schema) && value.length < schema.minLength)
27
+ value = value.padEnd(schema.minLength, 'x');
28
+ if (hasMaxLength(schema) && value.length > schema.maxLength)
29
+ value = value.slice(0, schema.maxLength);
30
+ return value;
31
+ };
32
+ /**
33
+ * Derives a single concrete, schema-valid value from a JSON Schema.
34
+ *
35
+ * Prefers explicit hints in this order: `const`, `examples[0]`, `default`,
36
+ * `enum[0]`; otherwise produces a canonical value for the declared type.
37
+ * `$ref`s are resolved and inlined by value; recursive refs short-circuit to
38
+ * `null` (tracked via `seen`).
39
+ *
40
+ * Note: values constrained only by `pattern` are not guaranteed to match the
41
+ * pattern — use the generated arbitrary when pattern fidelity matters.
42
+ */
43
+ export const deriveExample = (schema, rootSchema, seen = new Set()) => {
44
+ if (!isSchemaObject(schema))
45
+ return null;
46
+ if (hasConst(schema))
47
+ return schema.const;
48
+ if (hasExamples(schema) && Array.isArray(schema.examples) && schema.examples.length > 0)
49
+ return schema.examples[0];
50
+ if (hasDefault(schema))
51
+ return schema.default;
52
+ if (hasEnum(schema) && schema.enum.length > 0)
53
+ return schema.enum[0];
54
+ if (hasRef(schema)) {
55
+ const ref = schema.$ref;
56
+ if (seen.has(ref) || !rootSchema)
57
+ return null;
58
+ const resolved = resolveRef(ref, rootSchema);
59
+ if (!resolved)
60
+ return null;
61
+ return deriveExample(resolved, rootSchema, new Set([...seen, ref]));
62
+ }
63
+ const instanceOf = getMjstInstanceOf(schema);
64
+ if (instanceOf === 'Date')
65
+ return new Date(0);
66
+ const primitive = getMjstPrimitive(schema);
67
+ if (primitive === 'bigint')
68
+ return 0n;
69
+ if (hasOneOf(schema) && schema.oneOf[0] !== undefined)
70
+ return deriveExample(schema.oneOf[0], rootSchema, seen);
71
+ if (hasAnyOf(schema) && schema.anyOf[0] !== undefined)
72
+ return deriveExample(schema.anyOf[0], rootSchema, seen);
73
+ // `hasType` only matches a single string `type`; multi-type schemas fall
74
+ // through to `null`.
75
+ if (!hasType(schema))
76
+ return null;
77
+ switch (schema.type) {
78
+ case 'string':
79
+ return exampleString(schema);
80
+ case 'number':
81
+ case 'integer':
82
+ return hasMinimum(schema) ? schema.minimum : 0;
83
+ case 'boolean':
84
+ return true;
85
+ case 'null':
86
+ return null;
87
+ case 'array': {
88
+ const item = hasItems(schema) ? deriveExample(schema.items, rootSchema, seen) : null;
89
+ const count = hasMinItems(schema) ? Math.max(schema.minItems, 1) : 1;
90
+ return Array.from({ length: count }, () => item);
91
+ }
92
+ case 'object': {
93
+ const out = {};
94
+ if (hasProperties(schema)) {
95
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
96
+ out[key] = deriveExample(propSchema, rootSchema, seen);
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ default:
102
+ return null;
103
+ }
104
+ };
105
+ /**
106
+ * Serializes a derived value into a TypeScript source expression. Handles the
107
+ * non-JSON values `deriveExample` can produce (`Date`, `bigint`) in addition to
108
+ * plain JSON.
109
+ */
110
+ export const serializeValue = (value) => {
111
+ if (typeof value === 'bigint')
112
+ return `${value}n`;
113
+ if (value instanceof Date)
114
+ return `new Date(${JSON.stringify(value.toISOString())})`;
115
+ if (Array.isArray(value))
116
+ return `[${value.map(serializeValue).join(', ')}]`;
117
+ if (value !== null && typeof value === 'object') {
118
+ const entries = Object.entries(value)
119
+ .filter(([, v]) => v !== undefined)
120
+ .map(([key, v]) => `${JSON.stringify(key)}: ${serializeValue(v)}`);
121
+ return `{ ${entries.join(', ')} }`;
122
+ }
123
+ return JSON.stringify(value);
124
+ };
125
+ /**
126
+ * Generates an exported const holding a concrete, schema-valid example value.
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * generateExampleConst({ type: 'object', properties: { name: { type: 'string' } } }, 'Info')
131
+ * // export const infoExample: Info = { "name": "string" }
132
+ * ```
133
+ */
134
+ export const generateExampleConst = (schema, typeName, rootSchema) => {
135
+ const value = deriveExample(schema, rootSchema);
136
+ return `export const ${exampleName(typeName)}: ${typeName} = ${serializeValue(value)}`;
137
+ };
@@ -0,0 +1,12 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Generates a `fast-check` arbitrary that produces schema-valid values.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * generateArbitrary({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, 'Info')
8
+ * // export const InfoArbitrary: fc.Arbitrary<Info> = fc.record({ "name": fc.string() })
9
+ * ```
10
+ */
11
+ export declare const generateArbitrary: (schema: JSONSchema, typeName: string, suffix?: string) => string;
12
+ //# sourceMappingURL=generate-arbitrary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-arbitrary.d.ts","sourceRoot":"","sources":["../../src/generators/generate-arbitrary.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AA2JjE;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,WAAY,UAAU,YAAY,MAAM,sBAAgB,MAGrF,CAAA"}
@@ -0,0 +1,165 @@
1
+ import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
2
+ import { refToName } from '@amritk/helpers/ref-to-name';
3
+ 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';
4
+ /**
5
+ * Derives the arbitrary const name from a type name.
6
+ * e.g. "User" → "UserArbitrary"
7
+ */
8
+ const arbitraryName = (typeName) => `${typeName}Arbitrary`;
9
+ /** Builds a `fc.string({ ... })` expression honouring format and length constraints. */
10
+ const stringExpr = (schema) => {
11
+ if (hasFormat(schema)) {
12
+ switch (schema.format) {
13
+ case 'email':
14
+ return 'fc.emailAddress()';
15
+ case 'uuid':
16
+ return 'fc.uuid()';
17
+ case 'uri':
18
+ case 'url':
19
+ return 'fc.webUrl()';
20
+ case 'date-time':
21
+ return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString())';
22
+ case 'date':
23
+ return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(0, 10))';
24
+ }
25
+ }
26
+ if (hasPattern(schema))
27
+ return `fc.stringMatching(/${schema.pattern}/)`;
28
+ const opts = [];
29
+ if (hasMinLength(schema))
30
+ opts.push(`minLength: ${schema.minLength}`);
31
+ if (hasMaxLength(schema))
32
+ opts.push(`maxLength: ${schema.maxLength}`);
33
+ return opts.length > 0 ? `fc.string({ ${opts.join(', ')} })` : 'fc.string()';
34
+ };
35
+ /** Builds a `fc.integer({ ... })` expression honouring range and multiple-of constraints. */
36
+ const integerExpr = (schema) => {
37
+ const opts = [];
38
+ if (hasMinimum(schema))
39
+ opts.push(`min: ${schema.minimum}`);
40
+ else if (hasExclusiveMinimum(schema))
41
+ opts.push(`min: ${Number(schema.exclusiveMinimum) + 1}`);
42
+ if (hasMaximum(schema))
43
+ opts.push(`max: ${schema.maximum}`);
44
+ else if (hasExclusiveMaximum(schema))
45
+ opts.push(`max: ${Number(schema.exclusiveMaximum) - 1}`);
46
+ const base = opts.length > 0 ? `fc.integer({ ${opts.join(', ')} })` : 'fc.integer()';
47
+ return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base;
48
+ };
49
+ /** Builds a `fc.double({ ... })` expression honouring range and multiple-of constraints. */
50
+ const numberExpr = (schema) => {
51
+ const opts = ['noNaN: true', 'noDefaultInfinity: true'];
52
+ if (hasMinimum(schema))
53
+ opts.push(`min: ${schema.minimum}`);
54
+ else if (hasExclusiveMinimum(schema))
55
+ opts.push(`min: ${schema.exclusiveMinimum}`, 'minExcluded: true');
56
+ if (hasMaximum(schema))
57
+ opts.push(`max: ${schema.maximum}`);
58
+ else if (hasExclusiveMaximum(schema))
59
+ opts.push(`max: ${schema.exclusiveMaximum}`, 'maxExcluded: true');
60
+ const base = `fc.double({ ${opts.join(', ')} })`;
61
+ return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base;
62
+ };
63
+ /** Builds a `fc.array(...)` / `fc.uniqueArray(...)` expression for an array schema. */
64
+ const arrayExpr = (schema, suffix) => {
65
+ const items = hasItems(schema) && isSchemaObject(schema.items) ? arbitraryExpr(schema.items, suffix) : 'fc.anything()';
66
+ const opts = [];
67
+ if (hasMinItems(schema))
68
+ opts.push(`minLength: ${schema.minItems}`);
69
+ if (hasMaxItems(schema))
70
+ opts.push(`maxLength: ${schema.maxItems}`);
71
+ const fn = hasUniqueItems(schema) && schema.uniqueItems === true ? 'fc.uniqueArray' : 'fc.array';
72
+ return opts.length > 0 ? `${fn}(${items}, { ${opts.join(', ')} })` : `${fn}(${items})`;
73
+ };
74
+ /** Builds a `fc.record(...)` expression for an object schema. */
75
+ const objectExpr = (schema, suffix) => {
76
+ if (!hasProperties(schema))
77
+ return 'fc.object()';
78
+ const required = new Set(hasRequired(schema) ? schema.required : []);
79
+ const keys = Object.keys(schema.properties);
80
+ const entries = Object.entries(schema.properties).map(([key, propSchema]) => `${JSON.stringify(key)}: ${arbitraryExpr(propSchema, suffix)}`);
81
+ if (entries.length === 0)
82
+ return 'fc.record({})';
83
+ const model = `{ ${entries.join(', ')} }`;
84
+ // fc.record treats all keys as required by default. Only emit requiredKeys
85
+ // when at least one property is optional.
86
+ if (keys.every((key) => required.has(key)))
87
+ return `fc.record(${model})`;
88
+ const requiredKeys = [...required].map((key) => JSON.stringify(key)).join(', ');
89
+ return `fc.record(${model}, { requiredKeys: [${requiredKeys}] })`;
90
+ };
91
+ /** Builds a `fc.oneof(...)` expression from a list of branch schemas. */
92
+ const oneofExpr = (branches, suffix) => {
93
+ const exprs = branches.map((branch) => arbitraryExpr(branch, suffix));
94
+ return `fc.oneof(${exprs.join(', ')})`;
95
+ };
96
+ /** Builds the fast-check expression for a single (non-union) JSON Schema type. */
97
+ const scalarExpr = (type, schema, suffix) => {
98
+ switch (type) {
99
+ case 'string':
100
+ return stringExpr(schema);
101
+ case 'integer':
102
+ return integerExpr(schema);
103
+ case 'number':
104
+ return numberExpr(schema);
105
+ case 'boolean':
106
+ return 'fc.boolean()';
107
+ case 'null':
108
+ return 'fc.constant(null)';
109
+ case 'array':
110
+ return arrayExpr(schema, suffix);
111
+ case 'object':
112
+ return objectExpr(schema, suffix);
113
+ default:
114
+ return 'fc.anything()';
115
+ }
116
+ };
117
+ /**
118
+ * Recursively builds the fast-check arbitrary expression for a schema node.
119
+ * `$ref`s resolve to the referenced file's exported arbitrary; everything else
120
+ * maps to the appropriate `fc.*` combinator.
121
+ */
122
+ const arbitraryExpr = (schema, suffix) => {
123
+ if (!isSchemaObject(schema))
124
+ return 'fc.anything()';
125
+ if (hasRef(schema))
126
+ return arbitraryName(refToName(schema.$ref, suffix));
127
+ if (hasConst(schema))
128
+ return `fc.constant(${JSON.stringify(schema.const)})`;
129
+ if (hasEnum(schema)) {
130
+ const values = schema.enum.map((value) => JSON.stringify(value)).join(', ');
131
+ return `fc.constantFrom(${values})`;
132
+ }
133
+ const instanceOf = getMjstInstanceOf(schema);
134
+ if (instanceOf === 'Date')
135
+ return 'fc.date({ noInvalidDate: true })';
136
+ if (instanceOf)
137
+ return 'fc.anything()';
138
+ const primitive = getMjstPrimitive(schema);
139
+ if (primitive === 'bigint')
140
+ return 'fc.bigInt()';
141
+ if (primitive)
142
+ return 'fc.anything()';
143
+ if (hasOneOf(schema))
144
+ return oneofExpr(schema.oneOf, suffix);
145
+ if (hasAnyOf(schema))
146
+ return oneofExpr(schema.anyOf, suffix);
147
+ // `hasType` only matches a single string `type`; multi-type schemas
148
+ // (`type: ['string', 'null']`) fall through to the permissive fallback.
149
+ if (hasType(schema))
150
+ return scalarExpr(schema.type, schema, suffix);
151
+ return 'fc.anything()';
152
+ };
153
+ /**
154
+ * Generates a `fast-check` arbitrary that produces schema-valid values.
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * generateArbitrary({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, 'Info')
159
+ * // export const InfoArbitrary: fc.Arbitrary<Info> = fc.record({ "name": fc.string() })
160
+ * ```
161
+ */
162
+ export const generateArbitrary = (schema, typeName, suffix = '') => {
163
+ const expr = arbitraryExpr(schema, suffix);
164
+ return `export const ${arbitraryName(typeName)}: fc.Arbitrary<${typeName}> = ${expr}`;
165
+ };
@@ -0,0 +1,43 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Options for controlling what gets generated in an example file.
4
+ */
5
+ type GenerateExampleFileOptions = {
6
+ /**
7
+ * The $ref path of the schema being generated (e.g. `#/$defs/address`).
8
+ * Prevents the file from importing itself.
9
+ */
10
+ readonly selfRef?: string;
11
+ /**
12
+ * The root schema document. Used to resolve `$ref`s when deriving a concrete
13
+ * example value, and to filter out unresolvable refs from the import list.
14
+ */
15
+ readonly rootSchema?: Record<string, unknown>;
16
+ /**
17
+ * Suffix appended to every type/arbitrary name derived from a `$ref`.
18
+ * Defaults to `''` (no suffix).
19
+ */
20
+ readonly typeSuffix?: string;
21
+ };
22
+ /**
23
+ * Generates a complete TypeScript example file from a JSON Schema.
24
+ *
25
+ * The file contains:
26
+ * - An import of `fast-check` and imports for any `$ref` types and arbitraries
27
+ * - The exported TypeScript type definition
28
+ * - An exported `fast-check` arbitrary (`FooArbitrary`)
29
+ * - An exported concrete example value (`fooExample`)
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const schema = { type: 'object', properties: { title: { type: 'string' } }, required: ['title'] }
34
+ * generateExampleFile(schema, 'Info')
35
+ * // import * as fc from 'fast-check'
36
+ * // export type Info = { title: string }
37
+ * // export const InfoArbitrary: fc.Arbitrary<Info> = fc.record({ "title": fc.string() })
38
+ * // export const infoExample: Info = { "title": "string" }
39
+ * ```
40
+ */
41
+ export declare const generateExampleFile: (schema: JSONSchema, typeName: string, options?: GenerateExampleFileOptions) => string;
42
+ export {};
43
+ //# sourceMappingURL=generate-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-files.d.ts","sourceRoot":"","sources":["../../src/generators/generate-files.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAMjE;;GAEG;AACH,KAAK,0BAA0B,GAAG;IAChC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7C;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,mBAAmB,WACtB,UAAU,YACR,MAAM,YACN,0BAA0B,KACnC,MAsBF,CAAA"}
@@ -0,0 +1,41 @@
1
+ import { generateTypeDefinition } from '@amritk/helpers/generate-type-definition';
2
+ import { collectExampleImports } from './collect-example-imports.js';
3
+ import { generateExampleConst } from './derive-example.js';
4
+ import { generateArbitrary } from './generate-arbitrary.js';
5
+ /**
6
+ * Generates a complete TypeScript example file from a JSON Schema.
7
+ *
8
+ * The file contains:
9
+ * - An import of `fast-check` and imports for any `$ref` types and arbitraries
10
+ * - The exported TypeScript type definition
11
+ * - An exported `fast-check` arbitrary (`FooArbitrary`)
12
+ * - An exported concrete example value (`fooExample`)
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const schema = { type: 'object', properties: { title: { type: 'string' } }, required: ['title'] }
17
+ * generateExampleFile(schema, 'Info')
18
+ * // import * as fc from 'fast-check'
19
+ * // export type Info = { title: string }
20
+ * // export const InfoArbitrary: fc.Arbitrary<Info> = fc.record({ "title": fc.string() })
21
+ * // export const infoExample: Info = { "title": "string" }
22
+ * ```
23
+ */
24
+ export const generateExampleFile = (schema, typeName, options) => {
25
+ const typeSuffix = options?.typeSuffix ?? '';
26
+ const refImports = collectExampleImports(schema, {
27
+ selfRef: options?.selfRef,
28
+ rootSchema: options?.rootSchema,
29
+ typeSuffix,
30
+ });
31
+ const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix });
32
+ const arbitrary = generateArbitrary(schema, typeName, typeSuffix);
33
+ const example = generateExampleConst(schema, typeName, options?.rootSchema);
34
+ let result = `import * as fc from 'fast-check'\n`;
35
+ for (const imp of refImports) {
36
+ result += imp + '\n';
37
+ }
38
+ result += '\n';
39
+ result += typeDefinition + '\n\n' + arbitrary + '\n\n' + example + '\n';
40
+ return result;
41
+ };
@@ -0,0 +1,5 @@
1
+ export type { GeneratedFile } from './generators/build-schema.js';
2
+ export { buildExampleSchema } from './generators/build-schema.js';
3
+ export { deriveExample, generateExampleConst, serializeValue } from './generators/derive-example.js';
4
+ export { generateArbitrary } from './generators/generate-arbitrary.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AACjG,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { buildExampleSchema } from './generators/build-schema.js';
2
+ export { deriveExample, generateExampleConst, serializeValue } from './generators/derive-example.js';
3
+ export { generateArbitrary } from './generators/generate-arbitrary.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-examples",
3
- "version": "0.0.0",
3
+ "version": "0.2.0",
4
4
  "description": "Generate fast-check arbitraries and example values from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -33,23 +33,23 @@
33
33
  "access": "public"
34
34
  },
35
35
  "scripts": {
36
- "build": "bun run build:code && bun run build:types",
37
- "build:code": "bun build ./src/index.ts --outdir=dist --target=node",
38
- "build:types": "tsc -p ."
36
+ "build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f",
37
+ "types:check": "tsgo -p . --noEmit",
38
+ "test": "NODE_ENV=production vitest run --root ../.. generate-examples"
39
39
  },
40
40
  "imports": {
41
41
  "#generators/*": "./src/generators/*.ts"
42
42
  },
43
43
  "exports": {
44
44
  ".": {
45
- "bun": "./src/index.ts",
45
+ "development": "./src/index.ts",
46
46
  "default": "./dist/index.js",
47
47
  "types": "./dist/index.d.ts"
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "json-schema-typed": "catalog:",
52
- "@amritk/helpers": "workspace:*"
51
+ "json-schema-typed": "^8.0.1",
52
+ "@amritk/helpers": "0.7.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "fast-check": ">=3"
@@ -1,10 +1,5 @@
1
- import { buildDynamicRefMap } from '@amritk/helpers/build-dynamic-ref-map'
2
- import { extractRefs } from '@amritk/helpers/extract-refs'
3
- import { refToFilename } from '@amritk/helpers/ref-to-filename'
4
- import { refToName } from '@amritk/helpers/ref-to-name'
5
- import { resolveDynamicRefs } from '@amritk/helpers/resolve-dynamic-refs'
6
- import { resolveRef } from '@amritk/helpers/resolve-ref'
7
- import { upgradeDraft07Schema } from '@amritk/helpers/upgrade-draft07-schema'
1
+ import { generateIndexBarrel } from '@amritk/helpers/generate-index-barrel'
2
+ import { walkRefGraph } from '@amritk/helpers/walk-ref-graph'
8
3
  import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
9
4
 
10
5
  import { generateExampleFile } from './generate-files'
@@ -19,7 +14,8 @@ export type GeneratedFile = {
19
14
 
20
15
  /**
21
16
  * Builds all TypeScript example files from a JSON Schema by traversing all
22
- * $ref references recursively, mirroring the generate-parsers pipeline.
17
+ * `$ref` / `$dynamicRef` references recursively (via the shared
18
+ * `@amritk/helpers/walk-ref-graph` walker).
23
19
  *
24
20
  * Each generated file exports:
25
21
  * - A TypeScript type definition
@@ -45,83 +41,22 @@ export const buildExampleSchema = async (
45
41
  rootTypeName: string,
46
42
  typeSuffix = '',
47
43
  ): Promise<GeneratedFile[]> => {
48
- rootSchema = upgradeDraft07Schema(rootSchema as Record<string, unknown>) as JSONSchema
49
-
50
44
  const files: GeneratedFile[] = []
51
- const processedRefs = new Set<string>()
52
- const processedFilenames = new Set<string>()
53
- const refsToProcess: string[] = []
54
-
55
- const dynamicRefMap = buildDynamicRefMap(rootSchema)
56
-
57
- // Root schema
58
- const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap)
59
- const rootContent = generateExampleFile(processedRootSchema, rootTypeName, {
60
- rootSchema: rootSchema as Record<string, unknown>,
61
- typeSuffix,
62
- })
63
- const rootFilename = rootTypeName.toLowerCase()
64
45
 
65
- if (rootFilename !== 'index') {
66
- processedFilenames.add(rootFilename)
67
- files.push({ filename: `${rootFilename}.ts`, content: rootContent })
68
- }
46
+ walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
47
+ // `index` is reserved for the barrel below, so never let a definition of
48
+ // that name overwrite it.
49
+ if (node.filename === 'index') return
69
50
 
70
- const rootRefs = extractRefs(rootSchema)
71
- refsToProcess.push(...rootRefs)
72
-
73
- while (refsToProcess.length > 0) {
74
- const ref = refsToProcess.shift()
75
- if (!ref || processedRefs.has(ref)) continue
76
- processedRefs.add(ref)
77
-
78
- const resolvedSchema = resolveRef(ref, rootSchema as Record<string, unknown>)
79
- if (!resolvedSchema) {
80
- console.warn(`Warning: Could not resolve ref: ${ref}`)
81
- continue
82
- }
83
-
84
- const typeName = refToName(ref, typeSuffix)
85
- const filename = refToFilename(ref)
86
- const processedSchema = resolveDynamicRefs(resolvedSchema as JSONSchema, dynamicRefMap)
87
- const content = generateExampleFile(processedSchema, typeName, {
88
- selfRef: ref,
89
- rootSchema: rootSchema as Record<string, unknown>,
51
+ const content = generateExampleFile(node.schema, node.typeName, {
52
+ rootSchema: node.rootSchema,
90
53
  typeSuffix,
54
+ ...(node.ref !== undefined ? { selfRef: node.ref } : {}),
91
55
  })
56
+ files.push({ filename: `${node.filename}.ts`, content })
57
+ })
92
58
 
93
- if (filename !== 'index' && !processedFilenames.has(filename)) {
94
- processedFilenames.add(filename)
95
- files.push({ filename: `${filename}.ts`, content })
96
- }
97
-
98
- for (const nestedRef of extractRefs(resolvedSchema as JSONSchema)) {
99
- if (!processedRefs.has(nestedRef)) refsToProcess.push(nestedRef)
100
- }
101
- }
102
-
103
- // Generate index.ts barrel
104
- const TYPE_EXPORT_RE = /^export type (\w+)/gm
105
- const CONST_EXPORT_RE = /^export const (\w+)/gm
106
-
107
- const sortedFiles = [...files].sort((a, b) => a.filename.localeCompare(b.filename))
108
- let indexContent = ''
109
-
110
- for (const file of sortedFiles) {
111
- const moduleName = file.filename.replace(/\.ts$/, '')
112
- const typeNames: string[] = []
113
- const constNames: string[] = []
114
-
115
- for (const match of file.content.matchAll(TYPE_EXPORT_RE)) typeNames.push(match[1] as string)
116
- for (const match of file.content.matchAll(CONST_EXPORT_RE)) constNames.push(match[1] as string)
117
-
118
- if (typeNames.length === 0 && constNames.length === 0) continue
119
-
120
- const typeExports = typeNames.map((n) => `type ${n}`)
121
- indexContent += `export { ${[...typeExports, ...constNames].join(', ')} } from './${moduleName}';\n`
122
- }
123
-
124
- files.push({ filename: 'index.ts', content: indexContent })
59
+ files.push({ filename: 'index.ts', content: generateIndexBarrel(files) })
125
60
 
126
61
  return files
127
62
  }