@amritk/generate-examples 0.5.2 → 0.5.4
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/AI.md +37 -0
- package/dist/generators/build-schema.js +23 -50
- package/dist/generators/collect-example-imports.js +64 -94
- package/dist/generators/derive-example.js +507 -680
- package/dist/generators/find-schema-cycles.js +91 -115
- package/dist/generators/generate-arbitrary.js +316 -471
- package/dist/generators/generate-files.js +28 -45
- package/dist/generators/schema-validation.js +55 -78
- package/dist/index.js +10 -3
- package/package.json +6 -5
package/AI.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @amritk/generate-examples — notes for AI coding agents
|
|
2
|
+
|
|
3
|
+
Programmatic API: turn a JSON Schema into test data — a fast-check arbitrary
|
|
4
|
+
(`FooArbitrary`) and a concrete example value (`fooExample`) per node, plus
|
|
5
|
+
types. Full reference is [README.md](./README.md).
|
|
6
|
+
|
|
7
|
+
> Pre-alpha: APIs and generated output change pre-1.0.
|
|
8
|
+
|
|
9
|
+
## Minimal example
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { buildExampleSchema } from '@amritk/generate-examples'
|
|
13
|
+
|
|
14
|
+
const schema = {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: { id: { type: 'string', format: 'uuid' }, age: { type: 'integer', minimum: 0 } },
|
|
17
|
+
required: ['id'],
|
|
18
|
+
} as const
|
|
19
|
+
|
|
20
|
+
const files = await buildExampleSchema(schema, 'User') // → user.ts, index.ts
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Gotchas — where agents fail
|
|
24
|
+
|
|
25
|
+
1. **Generated arbitrary files `import * as fc from 'fast-check'`** — `fast-check`
|
|
26
|
+
(`>=3`) is an **optional peer dependency** consumers must install. The static
|
|
27
|
+
`fooExample` values have no runtime deps.
|
|
28
|
+
2. **`generateArbitrary` / `generateExampleConst` return source-code STRINGS**;
|
|
29
|
+
**`deriveExample` returns an actual runtime VALUE.** Easy to confuse.
|
|
30
|
+
3. **A static example constrained only by `pattern` may not match the pattern** —
|
|
31
|
+
use the arbitrary when pattern fidelity matters.
|
|
32
|
+
4. **Unsupported keywords degrade silently:** `fc.anything()` in arbitraries,
|
|
33
|
+
`null` in static examples — no error thrown.
|
|
34
|
+
|
|
35
|
+
Exports: `buildExampleSchema`, `generateArbitrary`, `generateExampleConst`,
|
|
36
|
+
`deriveExample`, `serializeValue`, `GeneratedFile`. Only the `.` entry.
|
|
37
|
+
Install: `bun add @amritk/generate-examples`.
|
|
@@ -1,52 +1,25 @@
|
|
|
1
|
-
import { generateIndexBarrel } from
|
|
2
|
-
import { walkRefGraph } from
|
|
3
|
-
import { findSchemaCycles } from
|
|
4
|
-
import { generateExampleFile } from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
* @param rootSchema - The root JSON Schema to build from
|
|
19
|
-
* @param rootTypeName - The name for the root type (e.g. "Document")
|
|
20
|
-
* @param typeSuffix - Suffix appended to every `$ref`-derived name (default `''`)
|
|
21
|
-
* @returns An array of generated TypeScript files
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```typescript
|
|
25
|
-
* const files = await buildExampleSchema(schema, 'Document')
|
|
26
|
-
* // files → [{ filename: 'document.ts', content: '...' }, { filename: 'index.ts', ... }]
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export const buildExampleSchema = async (rootSchema, rootTypeName, typeSuffix = '') => {
|
|
30
|
-
const files = [];
|
|
31
|
-
// Cross-file `$ref` cycles (A→B→A across modules) must emit lazy references
|
|
32
|
-
// for their cycle edges; eager top-level references would crash with a
|
|
33
|
-
// circular-ESM TDZ error at import. Detect the cycles up front so each file
|
|
34
|
-
// knows which siblings to defer.
|
|
35
|
-
const cycles = findSchemaCycles(rootSchema, rootTypeName, typeSuffix);
|
|
36
|
-
walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
|
|
37
|
-
// `index` is reserved for the barrel below, so never let a definition of
|
|
38
|
-
// that name overwrite it.
|
|
39
|
-
if (node.filename === 'index')
|
|
40
|
-
return;
|
|
41
|
-
const lazyRefFilenames = cycles.get(node.filename);
|
|
42
|
-
const content = generateExampleFile(node.schema, node.typeName, {
|
|
43
|
-
rootSchema: node.rootSchema,
|
|
44
|
-
typeSuffix,
|
|
45
|
-
...(node.ref !== undefined ? { selfRef: node.ref } : {}),
|
|
46
|
-
...(lazyRefFilenames !== undefined ? { lazyRefFilenames } : {}),
|
|
47
|
-
});
|
|
48
|
-
files.push({ filename: `${node.filename}.ts`, content });
|
|
1
|
+
import { generateIndexBarrel } from "@amritk/helpers/generate-index-barrel";
|
|
2
|
+
import { walkRefGraph } from "@amritk/helpers/walk-ref-graph";
|
|
3
|
+
import { findSchemaCycles } from "./find-schema-cycles.js";
|
|
4
|
+
import { generateExampleFile } from "./generate-files.js";
|
|
5
|
+
const buildExampleSchema = async (rootSchema, rootTypeName, typeSuffix = "") => {
|
|
6
|
+
const files = [];
|
|
7
|
+
const cycles = findSchemaCycles(rootSchema, rootTypeName, typeSuffix);
|
|
8
|
+
walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
|
|
9
|
+
if (node.filename === "index")
|
|
10
|
+
return;
|
|
11
|
+
const lazyRefFilenames = cycles.get(node.filename);
|
|
12
|
+
const content = generateExampleFile(node.schema, node.typeName, {
|
|
13
|
+
rootSchema: node.rootSchema,
|
|
14
|
+
typeSuffix,
|
|
15
|
+
...node.ref !== void 0 ? { selfRef: node.ref } : {},
|
|
16
|
+
...lazyRefFilenames !== void 0 ? { lazyRefFilenames } : {}
|
|
49
17
|
});
|
|
50
|
-
files.push({ filename:
|
|
51
|
-
|
|
18
|
+
files.push({ filename: `${node.filename}.ts`, content });
|
|
19
|
+
});
|
|
20
|
+
files.push({ filename: "index.ts", content: generateIndexBarrel(files) });
|
|
21
|
+
return files;
|
|
22
|
+
};
|
|
23
|
+
export {
|
|
24
|
+
buildExampleSchema
|
|
52
25
|
};
|
|
@@ -1,100 +1,70 @@
|
|
|
1
|
-
import { refToFilename } from
|
|
2
|
-
import { refToName } from
|
|
3
|
-
import { resolveRef } from
|
|
4
|
-
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasOneOf, hasProperties, hasRef, isSchemaObject
|
|
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
|
-
*/
|
|
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, hasOneOf, hasProperties, hasRef, isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
9
5
|
const buildImport = (ref, suffix) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return `import { type ${typeName}, ${typeName}Arbitrary } from './${filename}.js'`;
|
|
6
|
+
const filename = refToFilename(ref);
|
|
7
|
+
const typeName = refToName(ref, suffix);
|
|
8
|
+
return `import { type ${typeName}, ${typeName}Arbitrary } from './${filename}.js'`;
|
|
14
9
|
};
|
|
15
|
-
/**
|
|
16
|
-
* Recursively collects every `$ref` string reachable through the schema surface
|
|
17
|
-
* that the type and arbitrary generators traverse. A generated file's single
|
|
18
|
-
* import block must cover every ref those generators emit, so this walks the
|
|
19
|
-
* *same* nested surface `arbitraryExpr` (generate-arbitrary.ts) descends into —
|
|
20
|
-
* combinator branches, object `properties`/`patternProperties`/
|
|
21
|
-
* `additionalProperties`, and array `items`/`prefixItems` (both the single-schema
|
|
22
|
-
* and tuple array forms) — not just the top level. Missing any of these emits a
|
|
23
|
-
* bare `XxxArbitrary` identifier (or a bare `Xxx` type) with no matching import,
|
|
24
|
-
* producing TypeScript that fails to compile.
|
|
25
|
-
*
|
|
26
|
-
* `$ref` nodes short-circuit (matching `arbitraryExpr`, which resolves a `$ref`
|
|
27
|
-
* and ignores sibling keywords), so recursion is bounded by the schema's own
|
|
28
|
-
* structural nesting and cannot loop on a self-referential ref.
|
|
29
|
-
*/
|
|
30
10
|
const collectRefs = (schema) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return refs;
|
|
11
|
+
if (!isSchemaObject(schema))
|
|
12
|
+
return [];
|
|
13
|
+
if (hasRef(schema))
|
|
14
|
+
return [schema.$ref];
|
|
15
|
+
const refs = [];
|
|
16
|
+
const visit = (sub) => {
|
|
17
|
+
refs.push(...collectRefs(sub));
|
|
18
|
+
};
|
|
19
|
+
if (hasOneOf(schema))
|
|
20
|
+
schema.oneOf.forEach(visit);
|
|
21
|
+
if (hasAnyOf(schema))
|
|
22
|
+
schema.anyOf.forEach(visit);
|
|
23
|
+
if (hasAllOf(schema))
|
|
24
|
+
schema.allOf.forEach(visit);
|
|
25
|
+
if (hasProperties(schema))
|
|
26
|
+
Object.values(schema.properties).forEach(visit);
|
|
27
|
+
const raw = schema;
|
|
28
|
+
const patternProperties = raw["patternProperties"];
|
|
29
|
+
if (typeof patternProperties === "object" && patternProperties !== null) {
|
|
30
|
+
Object.values(patternProperties).forEach(visit);
|
|
31
|
+
}
|
|
32
|
+
if (hasAdditionalProperties(schema) && isSchemaObject(schema.additionalProperties)) {
|
|
33
|
+
visit(schema.additionalProperties);
|
|
34
|
+
}
|
|
35
|
+
const prefixItems = raw["prefixItems"];
|
|
36
|
+
if (Array.isArray(prefixItems))
|
|
37
|
+
prefixItems.forEach(visit);
|
|
38
|
+
const items = raw["items"];
|
|
39
|
+
if (Array.isArray(items))
|
|
40
|
+
items.forEach(visit);
|
|
41
|
+
else if (isSchemaObject(items))
|
|
42
|
+
visit(items);
|
|
43
|
+
return refs;
|
|
65
44
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const imports = [];
|
|
84
|
-
for (const ref of refs) {
|
|
85
|
-
const filename = refToFilename(ref);
|
|
86
|
-
if (seen.has(filename))
|
|
87
|
-
continue;
|
|
88
|
-
if (selfFilename && filename === selfFilename)
|
|
89
|
-
continue;
|
|
90
|
-
// Skip refs that don't resolve in this schema (external / never generated)
|
|
91
|
-
if (rootSchema) {
|
|
92
|
-
const resolved = resolveRef(ref, rootSchema);
|
|
93
|
-
if (!resolved)
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
seen.add(filename);
|
|
97
|
-
imports.push(buildImport(ref, typeSuffix));
|
|
45
|
+
const collectExampleImports = (schema, options) => {
|
|
46
|
+
const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null;
|
|
47
|
+
const rootSchema = options?.rootSchema;
|
|
48
|
+
const typeSuffix = options?.typeSuffix ?? "";
|
|
49
|
+
const refs = collectRefs(schema);
|
|
50
|
+
const seen = /* @__PURE__ */ new Set();
|
|
51
|
+
const imports = [];
|
|
52
|
+
for (const ref of refs) {
|
|
53
|
+
const filename = refToFilename(ref);
|
|
54
|
+
if (seen.has(filename))
|
|
55
|
+
continue;
|
|
56
|
+
if (selfFilename && filename === selfFilename)
|
|
57
|
+
continue;
|
|
58
|
+
if (rootSchema) {
|
|
59
|
+
const resolved = resolveRef(ref, rootSchema);
|
|
60
|
+
if (!resolved)
|
|
61
|
+
continue;
|
|
98
62
|
}
|
|
99
|
-
|
|
63
|
+
seen.add(filename);
|
|
64
|
+
imports.push(buildImport(ref, typeSuffix));
|
|
65
|
+
}
|
|
66
|
+
return imports;
|
|
67
|
+
};
|
|
68
|
+
export {
|
|
69
|
+
collectExampleImports
|
|
100
70
|
};
|