@amritk/validation 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.
Files changed (84) hide show
  1. package/AI.md +92 -0
  2. package/LICENSE +21 -0
  3. package/README.md +184 -0
  4. package/dist/generate.d.ts +135 -0
  5. package/dist/generate.js +74 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +5 -0
  8. package/dist/parsers/generators/assert-supported-keywords.d.ts +30 -0
  9. package/dist/parsers/generators/assert-supported-keywords.js +87 -0
  10. package/dist/parsers/generators/build-schema.d.ts +111 -0
  11. package/dist/parsers/generators/build-schema.js +74 -0
  12. package/dist/parsers/generators/generate-deep-equal-check.d.ts +24 -0
  13. package/dist/parsers/generators/generate-deep-equal-check.js +35 -0
  14. package/dist/parsers/generators/generate-discriminated-union-validation.d.ts +15 -0
  15. package/dist/parsers/generators/generate-discriminated-union-validation.js +28 -0
  16. package/dist/parsers/generators/generate-enum-check.d.ts +35 -0
  17. package/dist/parsers/generators/generate-enum-check.js +31 -0
  18. package/dist/parsers/generators/generate-files.d.ts +138 -0
  19. package/dist/parsers/generators/generate-files.js +89 -0
  20. package/dist/parsers/generators/generate-non-discriminated-union-validation.d.ts +6 -0
  21. package/dist/parsers/generators/generate-non-discriminated-union-validation.js +26 -0
  22. package/dist/parsers/generators/generate-parser-function.d.ts +120 -0
  23. package/dist/parsers/generators/generate-parser-function.js +1973 -0
  24. package/dist/parsers/generators/generate-schema-checks.d.ts +5 -0
  25. package/dist/parsers/generators/generate-schema-checks.js +260 -0
  26. package/dist/parsers/generators/generate-strict-assertion.d.ts +155 -0
  27. package/dist/parsers/generators/generate-strict-assertion.js +759 -0
  28. package/dist/parsers/generators/generate-type-checks.d.ts +148 -0
  29. package/dist/parsers/generators/generate-type-checks.js +454 -0
  30. package/dist/parsers/generators/generate-unique-items-check.d.ts +16 -0
  31. package/dist/parsers/generators/generate-unique-items-check.js +32 -0
  32. package/dist/parsers/generators/generate-validation-expression.d.ts +79 -0
  33. package/dist/parsers/generators/generate-validation-expression.js +416 -0
  34. package/dist/parsers/generators/subschema-match.d.ts +48 -0
  35. package/dist/parsers/generators/subschema-match.js +485 -0
  36. package/dist/parsers/generators/unevaluated-match.d.ts +19 -0
  37. package/dist/parsers/generators/unevaluated-match.js +206 -0
  38. package/dist/parsers/helpers/apply-schema-extensions.d.ts +35 -0
  39. package/dist/parsers/helpers/apply-schema-extensions.js +20 -0
  40. package/dist/parsers/helpers/collect-helpers.d.ts +15 -0
  41. package/dist/parsers/helpers/collect-helpers.js +64 -0
  42. package/dist/parsers/helpers/collect-imports.d.ts +104 -0
  43. package/dist/parsers/helpers/collect-imports.js +227 -0
  44. package/dist/parsers/helpers/find-discriminator.d.ts +6 -0
  45. package/dist/parsers/helpers/find-discriminator.js +27 -0
  46. package/dist/parsers/helpers/get-discriminator-value.d.ts +6 -0
  47. package/dist/parsers/helpers/get-discriminator-value.js +18 -0
  48. package/dist/parsers/index.d.ts +2 -0
  49. package/dist/parsers/index.js +4 -0
  50. package/dist/parsers/types/schema-extensions.d.ts +34 -0
  51. package/dist/parsers/types/schema-extensions.js +0 -0
  52. package/dist/rehome-parser-file.d.ts +25 -0
  53. package/dist/rehome-parser-file.js +74 -0
  54. package/dist/validators/generators/assert-generatable-refs.d.ts +19 -0
  55. package/dist/validators/generators/assert-generatable-refs.js +12 -0
  56. package/dist/validators/generators/assert-unevaluated-generatable.d.ts +30 -0
  57. package/dist/validators/generators/assert-unevaluated-generatable.js +71 -0
  58. package/dist/validators/generators/build-schema.d.ts +61 -0
  59. package/dist/validators/generators/build-schema.js +666 -0
  60. package/dist/validators/generators/collect-emitted-refs.d.ts +26 -0
  61. package/dist/validators/generators/collect-emitted-refs.js +112 -0
  62. package/dist/validators/generators/collect-validator-imports.d.ts +86 -0
  63. package/dist/validators/generators/collect-validator-imports.js +62 -0
  64. package/dist/validators/generators/emit-format-checks.d.ts +50 -0
  65. package/dist/validators/generators/emit-format-checks.js +248 -0
  66. package/dist/validators/generators/enforced-keywords.d.ts +33 -0
  67. package/dist/validators/generators/enforced-keywords.js +70 -0
  68. package/dist/validators/generators/folds-to-constant.d.ts +15 -0
  69. package/dist/validators/generators/folds-to-constant.js +43 -0
  70. package/dist/validators/generators/generate-coerce-function.d.ts +22 -0
  71. package/dist/validators/generators/generate-coerce-function.js +208 -0
  72. package/dist/validators/generators/generate-files.d.ts +92 -0
  73. package/dist/validators/generators/generate-files.js +86 -0
  74. package/dist/validators/generators/generate-repair-function.d.ts +27 -0
  75. package/dist/validators/generators/generate-repair-function.js +102 -0
  76. package/dist/validators/generators/generate-validator-function.d.ts +58 -0
  77. package/dist/validators/generators/generate-validator-function.js +1691 -0
  78. package/dist/validators/generators/tuple-shape.d.ts +31 -0
  79. package/dist/validators/generators/tuple-shape.js +20 -0
  80. package/dist/validators/generators/unevaluated-match.d.ts +29 -0
  81. package/dist/validators/generators/unevaluated-match.js +223 -0
  82. package/dist/validators/index.d.ts +2 -0
  83. package/dist/validators/index.js +4 -0
  84. package/package.json +92 -0
package/AI.md ADDED
@@ -0,0 +1,92 @@
1
+ # @amritk/validation — notes for AI coding agents
2
+
3
+ One generator surface over mjst's types, guards, validators, coercers, repairers
4
+ and parsers. It owns both generator engines: the validator/coercer/repairer one
5
+ and the parser/type one, which used to ship as `@amritk/generate-validators` and
6
+ `@amritk/generate-parsers` and are now internal modules here.
7
+
8
+ > Pre-alpha: APIs and generated output change pre-1.0.
9
+
10
+ ## Minimal example
11
+
12
+ ```ts
13
+ import { generate } from '@amritk/validation'
14
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
15
+
16
+ const schema: JSONSchema = {
17
+ type: 'object',
18
+ properties: { title: { type: 'string' } },
19
+ required: ['title'],
20
+ }
21
+
22
+ const files = await generate(schema, 'Document', { modes: ['types', 'guard', 'validate', 'repair'] })
23
+ // → document.ts, validation-result.ts, index.ts
24
+ ```
25
+
26
+ ## The modes
27
+
28
+ | mode | emits | value back | tells you | stops at |
29
+ |:---|:---|:---|:---|:---|
30
+ | `types` | `X` | — | — | — |
31
+ | `guard` | `isX` | — | a boolean | first problem |
32
+ | `validate` | `validateX` | — | every error | the end |
33
+ | `check` | `checkX` | — | the first error | first problem |
34
+ | `coerce` | `coerceX` | coerced | every error | the end |
35
+ | `repair` | `repairX` | repaired | repairs **and** errors | the end |
36
+ | `parse` | `parseX` | repaired | nothing (total) | never |
37
+ | `parseStrict` | `parseX` | as given | throws the first | first problem |
38
+
39
+ ## The same modes from the CLI
40
+
41
+ Every mode is reachable from `mjst` without dropping to the programmatic API:
42
+
43
+ | mode | flags |
44
+ |:---|:---|
45
+ | `types` | `--types-only` (alone), otherwise always emitted |
46
+ | `guard`, `validate` | `--validators`, or `--validators-only` for no parser |
47
+ | `check` | `--check` |
48
+ | `coerce` | `--coerce` |
49
+ | `repair` | `--repair` (emits `coerceX` too) |
50
+ | `parse` | the default on any run that is not `--types-only`/`--validators-only` |
51
+ | `parseStrict` | `--strict` |
52
+
53
+ `--validators-only` is this package's own default (`['types', 'guard', 'validate']`)
54
+ spelled as a flag: everything that judges a document, nothing that builds one.
55
+
56
+ ## Gotchas — where agents fail
57
+
58
+ 1. **Options object, not positional booleans.** `generate(schema, typeName, options)`.
59
+ The two engines underneath take long positional argument lists; this is the
60
+ only entry point, and they are not reachable from outside the package.
61
+ 2. **The default is `['types', 'guard', 'validate']`** — read-only. Anything that
62
+ rewrites a document (`coerce`, `repair`, `parse`) is opt-in.
63
+ 3. **`parse` and `parseStrict` are mutually exclusive** and `generate` throws if
64
+ you ask for both: they are one function name under two contracts.
65
+ 4. **One type, two files.** The type is declared once, in `x.ts`, alongside the
66
+ validator half. The parser half lands in `x.parse.ts` and imports the type. Do
67
+ not expect `parseX` in `x.ts`, and do not expect a second `export type X`
68
+ anywhere — there is exactly one.
69
+ 5. **`repairX` reports `valid: true` for a *fully repaired* document**, with a
70
+ non-empty `repairs`. `result.valid` alone does not mean the input was clean;
71
+ check `repairs.length` when that matters.
72
+ 6. **`guard` is the fail-fast mode and it is much cheaper on bad input** — it
73
+ short-circuits and builds no error object. It is also, counter-intuitively,
74
+ *not* faster than `validateX` on input that is valid, where both must check
75
+ everything. Pick `guard` when the answer is a branch, `validate` when someone
76
+ has to be told what to fix.
77
+ 7. **`check` is the middle of those two, and it returns `validateX`'s type.**
78
+ `checkX` hands back the same `true | { valid: false, errors }`, with exactly
79
+ one error in it — the one `validateX` would have reported first, same `path`,
80
+ `keyword` and `params` — so error-handling code is shared between the two.
81
+ Pick it when a failure has to be reported but only the first thing wrong
82
+ matters. Do not build it by hand out of the other two: `isX(v) ? true :
83
+ validateX(v)` pays for both passes and is no faster than `validateX`.
84
+
85
+ 8. **It emits each engine's exact bytes.** A single-mode build is byte-identical
86
+ to what that engine emitted when it was its own package, so there is no runtime
87
+ difference to reason about and no speedup to claim — do not tell a user this is
88
+ faster. What it changes is that the whole matrix comes out as one type instead
89
+ of two, and that a build asking for no validator mode ships no
90
+ `validation-result.ts`.
91
+
92
+ Only the `.` entry. Install: `bun add @amritk/validation`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 amritk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ <div align="center">
2
+
3
+ # @amritk/validation
4
+
5
+ **One generator surface over mjst's types, guards, validators, coercers, repairers and parsers.**
6
+
7
+ </div>
8
+
9
+ ---
10
+
11
+ ## Overview
12
+
13
+ `@amritk/validation` is the front door. Given a JSON Schema (Draft 2020-12) it emits
14
+ one coherent set of TypeScript files carrying whichever runtime entry points you
15
+ ask for — from a bare type, through a boolean guard and a full error report, to a
16
+ coercer, a repairer and a total parser.
17
+
18
+ Underneath it are **two** generator engines, not one: a validator, coercer and
19
+ repairer engine, and a parser and type engine. They shipped separately as
20
+ `@amritk/generate-validators` and `@amritk/generate-parsers` until this package
21
+ absorbed them, and keeping both is deliberate. They emit genuinely different
22
+ code for the value-producing modes: a parser fuses building the output with
23
+ checking it, and is several times faster for it; a validator keeps the passes
24
+ apart, and can therefore report what was wrong. Collapsing them into one emitter
25
+ would mean giving up one of those properties.
26
+
27
+ What makes one output directory possible is that both derive the TypeScript type
28
+ from the same `@amritk/helpers/generate-type-definition`, so the type is declared
29
+ **once** and every function in the output is talking about it.
30
+
31
+ ---
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ bun add @amritk/validation
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Usage
42
+
43
+ ```typescript
44
+ import { generate } from '@amritk/validation'
45
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
46
+
47
+ const schema: JSONSchema = {
48
+ type: 'object',
49
+ properties: {
50
+ retries: { type: 'integer', minimum: 0, maximum: 10, default: 3 },
51
+ name: { type: 'string', minLength: 3 },
52
+ },
53
+ required: ['retries', 'name'],
54
+ }
55
+
56
+ const files = await generate(schema, 'Config', {
57
+ modes: ['types', 'guard', 'validate', 'coerce', 'repair', 'parse'],
58
+ })
59
+
60
+ for (const file of files) await Bun.write(`./generated/${file.filename}`, file.content)
61
+ ```
62
+
63
+ ---
64
+
65
+ ## The modes
66
+
67
+ Each mode is a different answer to *how much may this do to my document, and what
68
+ do I get told*. They compose: ask for several and you get several functions over
69
+ one type, and every one that judges a value judges it with the same `validateX`,
70
+ so no two can disagree about whether a document is valid.
71
+
72
+ | mode | function | value back | tells you | stops at |
73
+ |:---|:---|:---|:---|:---|
74
+ | `types` | — | — | — | — |
75
+ | `guard` | `isX(input): input is X` | — | a boolean | the first problem |
76
+ | `validate` | `validateX(input)` | — | every error | the end |
77
+ | `coerce` | `coerceX(input)` | coerced | every error | the end |
78
+ | `repair` | `repairX(input)` | repaired | repairs **and** errors | the end |
79
+ | `parse` | `parseX(input)` | repaired | nothing | never fails |
80
+ | `parseStrict` | `parseX(input)` | as given | throws the first | the first problem |
81
+
82
+ **Validate only, fail fast or find every error.** `guard` is the fail-fast half:
83
+ it stops at the first thing wrong and builds no error object, which on invalid
84
+ input is dramatically cheaper than reporting. `validate` is the other half: every
85
+ error, each with a JSON Pointer, the keyword that rejected the value, and that
86
+ keyword's own values. Reach for `guard` when the answer is a branch and
87
+ `validate` when a human or an API client has to be told what to fix.
88
+
89
+ **Coerce and repair, with or without the errors.** `coerceX` and `repairX` hand
90
+ back the value *and* the diagnostics; `parseX` hands back only the value and
91
+ never fails. Coercion moves a value that is already right but written in the
92
+ wrong type; repair also substitutes a value the schema supplies for one that
93
+ cannot be coerced, and reports the validator's own errors as the repairs.
94
+
95
+ `parse` and `parseStrict` are the same function name under two contracts, so
96
+ asking for both is an error rather than a silent choice.
97
+
98
+ ---
99
+
100
+ ## API
101
+
102
+ ### `generate(rootSchema, rootTypeName, options?)`
103
+
104
+ | Parameter | Type | Default | Description |
105
+ |:---|:---|:---|:---|
106
+ | `rootSchema` | `JSONSchema` | — | The root schema. `$ref` and `$dynamicRef` are resolved recursively. |
107
+ | `rootTypeName` | `string` | — | Name for the root type (e.g. `"Config"`). |
108
+ | `options.modes` | `Mode[]` | `['types', 'guard', 'validate']` | Which entry points to emit. The default is read-only; anything that rewrites a document is opt-in. |
109
+ | `options.typeSuffix` | `string` | `''` | Suffix on every `$ref`-derived type name. The root name is untouched. |
110
+ | `options.schemas` | `Record<string, unknown>` | — | Documents you have already loaded, keyed by the URI a `$ref` names them by. |
111
+ | `options.unknownKeys` | `'count-keys' \| 'count-enumerable'` | `'count-keys'` | How a closed object's fast path proves it carries no undeclared key. |
112
+ | `options.formats` | `'all' \| string[]` | — | String `format`s the validators enforce. Unset leaves `format` an annotation. |
113
+ | `options.branchErrors` | `boolean` | `false` | Explain a failing `anyOf` / `oneOf` with the branch it plainly meant. |
114
+ | `options.stripUnknown` | `boolean` | `false` | Build each parsed result from the declared properties only. |
115
+ | `options.readonly` | `boolean` | `false` | Emit `readonly` type members. |
116
+ | `options.caseInsensitive` | `boolean` | `false` | Normalize a mis-cased string onto an `enum` member it matches. |
117
+ | `options.helpersMode` | `'package' \| 'embedded'` | `'package'` | Where the parser half gets its runtime helpers. |
118
+ | `options.extensions` | `SchemaExtensions` | — | Schema extensions, passed through to the parser half. |
119
+
120
+ Returns: `Promise<GeneratedFile[]>` where `GeneratedFile = { filename: string; content: string }`.
121
+
122
+ ---
123
+
124
+ ## Output layout
125
+
126
+ ```
127
+ config.ts the type, plus isX / validateX / coerceX / repairX
128
+ config.parse.ts parseX, importing the type from ./config.js
129
+ validation-result.ts the shared error and result types, and the runtime helpers
130
+ index.ts a barrel over all of it
131
+ ```
132
+
133
+ The type lives in `config.ts` because that is where a reader looks for it. The
134
+ parser half moves to `config.parse.ts` because both generators name their file
135
+ after the schema and one of them has to give way. There is exactly one
136
+ `export type Config` in the output, whatever combination of modes you asked for
137
+ — which the test suite asserts, over a corpus of schema shapes, by compiling the
138
+ result under this repo's own flags.
139
+
140
+ Ask for no validator mode and there is nothing to collide with, so the parser
141
+ keeps both its own filename and the type it already authored, and no
142
+ `validation-result.ts` is emitted at all — a parse-only build has nothing that
143
+ could import its 17 KiB of error types and runtime helpers.
144
+
145
+ ---
146
+
147
+ ## Is it faster?
148
+
149
+ No, and it should not be. Ask this package for one mode and it emits **the exact
150
+ bytes** the engine that owns that mode emits — which the test suite pins per
151
+ mode, by fingerprint, not by reading the output. Identical code cannot
152
+ run at a different speed, so there is no runtime claim to make here and none is
153
+ made. `bun run bench` measures it anyway, and the deltas wander either side of
154
+ zero between runs, which is what process-level variance looks like when there is
155
+ no underlying difference.
156
+
157
+ What does change is the cold side, and only in the direction you would hope:
158
+
159
+ | | before (both engines directly) | after |
160
+ |:---|:---|:---|
161
+ | codegen for the whole matrix | 2.9 ms | 2.7 ms |
162
+ | emitted bytes | 46.0 KiB | 45.8 KiB |
163
+ | files | 7 | 6 |
164
+ | declarations of `Order` | **2** | **1** |
165
+ | per-mode codegen | — | equal or slightly lower |
166
+ | per-mode bytes | — | identical |
167
+
168
+ The one that matters is the last row of the middle block. Reaching every mode
169
+ used to mean running both generators yourself and shipping two trees for one
170
+ schema, including two declarations of the same type — structurally
171
+ interchangeable, but two things to keep in step. Now there is one.
172
+
173
+ ---
174
+
175
+ ## Related packages
176
+
177
+ - [`@amritk/helpers`](../helpers) — the shared type definition and fallback tables both engines read
178
+ - [`@amritk/mjst`](../cli) — the command-line interface
179
+
180
+ ---
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,135 @@
1
+ import { type UnknownKeysStrategy } from '@amritk/helpers/unknown-keys-strategy';
2
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
3
+ import { buildSchema } from './parsers/index.js';
4
+ /** The extension emitted on relative import specifiers. */
5
+ export type ImportExtension = 'js' | 'ts';
6
+ /** A generated TypeScript file, as every mjst generator hands one back. */
7
+ export type GeneratedFile = {
8
+ filename: string;
9
+ content: string;
10
+ };
11
+ /**
12
+ * One runtime entry point the generated output can carry. Each is a different
13
+ * answer to "how much may this do to my document, and what do I get told".
14
+ *
15
+ * They compose rather than compete: asking for several emits several functions
16
+ * over one shared type, and the ones that judge a value all judge it with the
17
+ * same `validateX`, so no two can disagree about whether a document is valid.
18
+ */
19
+ export type Mode =
20
+ /** `X` — the TypeScript type, and nothing that runs. Always emitted. */
21
+ 'types'
22
+ /**
23
+ * `isX(input): input is X` — a type guard. Stops at the first thing wrong and
24
+ * builds no error object, which on invalid input is dramatically cheaper than
25
+ * any mode that reports: the check that fails is usually the first one tried.
26
+ * Use it when the answer is a branch, not a message.
27
+ */
28
+ | 'guard'
29
+ /**
30
+ * `validateX(input): ValidationResult` — every error, each with a JSON Pointer,
31
+ * the keyword that rejected the value and that keyword's own values. Use it
32
+ * when a human or an API client has to be told what to fix.
33
+ */
34
+ | 'validate'
35
+ /**
36
+ * `checkX(input): ValidationResult` — the same result `validateX` returns, with
37
+ * the one error that stopped it. Use it when a failure has to be reported but
38
+ * only the first thing wrong matters: it costs a single error object and walks
39
+ * no further, where `validateX` keeps going to collect the rest.
40
+ */
41
+ | 'check'
42
+ /**
43
+ * `coerceX(input): CoercionResult<X>` — move a scalar toward the declared type
44
+ * where the schema leaves no choice, then validate. Substitutes nothing, so a
45
+ * value it cannot coerce is rejected as written.
46
+ */
47
+ | 'coerce'
48
+ /**
49
+ * `repairX(input): RepairResult<X>` — coerce, validate, then repair each
50
+ * rejected position to a value the schema supplies, reporting the validator's
51
+ * own errors as the repairs. Tolerant, and never silently so.
52
+ */
53
+ | 'repair'
54
+ /**
55
+ * `parseX(input): X` — a total function. Repairs whatever it is given and
56
+ * always returns a valid instance, reporting nothing. The fastest way to get a
57
+ * usable value when you do not care what was wrong with the input.
58
+ */
59
+ | 'parse'
60
+ /**
61
+ * `parseX(input): X` that throws on the first violation instead of repairing.
62
+ * Mutually exclusive with `'parse'` — they are the same function under two
63
+ * contracts.
64
+ */
65
+ | 'parseStrict';
66
+ /** Every mode, for a caller who wants the lot. */
67
+ export declare const ALL_MODES: readonly Mode[];
68
+ export type GenerateOptions = {
69
+ /**
70
+ * Which entry points to emit. Defaults to `['types', 'guard', 'validate']` —
71
+ * the modes that only ever read the document. Anything that rewrites a value
72
+ * is opt-in, because a generator that quietly starts repairing documents is
73
+ * not one you can trust by default.
74
+ */
75
+ readonly modes?: readonly Mode[];
76
+ /** Suffix appended to every `$ref`-derived type name. The root name is untouched. */
77
+ readonly typeSuffix?: string;
78
+ /** Documents already loaded, keyed by the absolute URI a `$ref` names them by. */
79
+ readonly schemas?: Readonly<Record<string, unknown>>;
80
+ /** How a closed object's fast path proves it carries no undeclared key. */
81
+ readonly unknownKeys?: UnknownKeysStrategy;
82
+ /** String `format`s the generated validators enforce. Unset leaves `format` an annotation. */
83
+ readonly formats?: 'all' | readonly string[];
84
+ /** Explain a failing `anyOf` / `oneOf` with the errors of the branch it plainly meant. */
85
+ readonly branchErrors?: boolean;
86
+ /** Build each parsed result from the declared properties only, dropping undeclared keys. */
87
+ readonly stripUnknown?: boolean;
88
+ /** Emit `readonly` type members. */
89
+ readonly readonly?: boolean;
90
+ /** Normalize a mis-cased string onto an `enum` member it matches case-insensitively. */
91
+ readonly caseInsensitive?: boolean;
92
+ /** Where the parser half gets its runtime helpers: the published package, or emitted sources. */
93
+ readonly helpersMode?: 'package' | 'embedded';
94
+ /** Prefix on emitted-helper import specifiers, when `helpersMode` is `'embedded'`. */
95
+ readonly helpersImportPrefix?: string;
96
+ /** Schema extensions, keyed by definition name, passed through to the parser half. */
97
+ readonly extensions?: Parameters<typeof buildSchema>[2];
98
+ /**
99
+ * Extension on every emitted relative import specifier. `'js'` (the default) is
100
+ * the standard NodeNext form a compiled consumer needs; `'ts'` emits the literal
101
+ * on-disk paths, so the output runs directly under Node's type stripping with no
102
+ * build step.
103
+ *
104
+ * It reaches both halves, which is not a detail: they share one directory here,
105
+ * and a set where the parser files say `.ts` and the validator files say `.js`
106
+ * is one that resolves under neither runtime.
107
+ */
108
+ readonly importExt?: ImportExtension;
109
+ /**
110
+ * Print warnings the parser generator raises about the schema — a keyword it
111
+ * cannot enforce, a construct it had to widen. Off by default because this is a
112
+ * library call, and a library that writes to stdout is a nuisance in anything
113
+ * that embeds it.
114
+ */
115
+ readonly logWarnings?: boolean;
116
+ };
117
+ /**
118
+ * Generates one coherent set of files for a schema, carrying whichever entry
119
+ * points were asked for.
120
+ *
121
+ * This composes the two generator engines that live beside it, `./parsers` and
122
+ * `./validators`, rather than reimplementing either. That is deliberate: the two
123
+ * emit genuinely different code for the value-producing modes — a parser fuses
124
+ * building the output with checking it and is several times faster for it, while
125
+ * a validator keeps the passes apart and can therefore report — and collapsing
126
+ * them into one emitter would mean giving up one of those properties. Composing
127
+ * keeps both and spends the cost on reconciling their output instead, which is
128
+ * bounded and testable.
129
+ *
130
+ * What makes the reconciliation honest is that both generators derive the type
131
+ * from the same `@amritk/helpers/generate-type-definition`, so the type is
132
+ * emitted once and the parser half imports it. There is exactly one `X` in the
133
+ * output, and every function in it is talking about the same type.
134
+ */
135
+ export declare const generate: (rootSchema: JSONSchema, rootTypeName: string, options?: GenerateOptions) => Promise<GeneratedFile[]>;
@@ -0,0 +1,74 @@
1
+ import { generateIndexBarrel } from "@amritk/helpers/generate-index-barrel";
2
+ import { DEFAULT_UNKNOWN_KEYS } from "@amritk/helpers/unknown-keys-strategy";
3
+ import { buildSchema } from "./parsers/index.js";
4
+ import { buildValidatorSchema } from "./validators/index.js";
5
+ import { rehomeParserFile } from "./rehome-parser-file.js";
6
+ const ALL_MODES = ["types", "guard", "validate", "check", "coerce", "repair", "parse"];
7
+ const VALIDATOR_MODES = /* @__PURE__ */ new Set(["guard", "validate", "check", "coerce", "repair"]);
8
+ const PARSER_MODES = /* @__PURE__ */ new Set(["parse", "parseStrict"]);
9
+ const PARSER_SUFFIX = ".parse";
10
+ const wants = (modes, group) => modes.some((mode) => group.has(mode));
11
+ const generate = async (rootSchema, rootTypeName, options = {}) => {
12
+ const modes = options.modes ?? ["types", "guard", "validate"];
13
+ if (modes.includes("parse") && modes.includes("parseStrict")) {
14
+ throw new Error("modes 'parse' and 'parseStrict' are the same function under two contracts \u2014 one repairs and always returns a value, the other throws on the first violation. Ask for one.");
15
+ }
16
+ const typeSuffix = options.typeSuffix ?? "";
17
+ const unknownKeys = options.unknownKeys ?? DEFAULT_UNKNOWN_KEYS;
18
+ const importExt = options.importExt ?? "js";
19
+ const needsValidator = wants(modes, VALIDATOR_MODES);
20
+ const needsParser = wants(modes, PARSER_MODES);
21
+ const files = [];
22
+ const parserFiles = async () => buildSchema(
23
+ rootSchema,
24
+ rootTypeName,
25
+ options.extensions,
26
+ !needsParser,
27
+ // typesOnly — when the parser is only here to author the type
28
+ options.logWarnings === true,
29
+ modes.includes("parseStrict"),
30
+ options.helpersMode ?? "package",
31
+ options.helpersImportPrefix ?? "./",
32
+ options.readonly === true,
33
+ options.stripUnknown === true,
34
+ typeSuffix,
35
+ importExt,
36
+ options.caseInsensitive === true,
37
+ options.schemas,
38
+ unknownKeys
39
+ );
40
+ if (!needsValidator) {
41
+ for (const file of await parserFiles()) {
42
+ if (file.filename !== "index.ts")
43
+ files.push(file);
44
+ }
45
+ files.push({ filename: "index.ts", content: generateIndexBarrel(files, { importExt }) });
46
+ return files;
47
+ }
48
+ const validatorFiles = await buildValidatorSchema(rootSchema, rootTypeName, typeSuffix, options.schemas, unknownKeys, options.formats, modes.includes("coerce") || modes.includes("repair"), options.branchErrors === true, modes.includes("repair"), importExt, modes.includes("check"));
49
+ for (const file of validatorFiles) {
50
+ if (file.filename !== "index.ts")
51
+ files.push(file);
52
+ }
53
+ if (needsParser) {
54
+ for (const file of await parserFiles()) {
55
+ if (file.filename === "index.ts")
56
+ continue;
57
+ if (file.filename.includes("/")) {
58
+ files.push(file);
59
+ continue;
60
+ }
61
+ const moduleName = file.filename.replace(/\.ts$/, "");
62
+ files.push({
63
+ filename: `${moduleName}${PARSER_SUFFIX}.ts`,
64
+ content: rehomeParserFile(file.content, moduleName, PARSER_SUFFIX, importExt)
65
+ });
66
+ }
67
+ }
68
+ files.push({ filename: "index.ts", content: generateIndexBarrel(files, { importExt }) });
69
+ return files;
70
+ };
71
+ export {
72
+ ALL_MODES,
73
+ generate
74
+ };
@@ -0,0 +1 @@
1
+ export { ALL_MODES, type GeneratedFile, type GenerateOptions, generate, type ImportExtension, type Mode, } from './generate.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { ALL_MODES, generate } from "./generate.js";
2
+ export {
3
+ ALL_MODES,
4
+ generate
5
+ };
@@ -0,0 +1,30 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ import { type StrictAssertionContext } from './generate-strict-assertion.js';
3
+ /**
4
+ * Throws when a schema (anywhere in its subtree) uses a keyword the *strict*
5
+ * parser generator cannot enforce but which narrows the set of valid documents.
6
+ * Mirrors the validator engine's `assertNoUnsupportedKeywords`: failing loudly at
7
+ * generation time is strictly better than silently emitting a parser that
8
+ * accepts input the schema forbids — a strict parser promises to "throw on
9
+ * violations".
10
+ *
11
+ * Two families are rejected:
12
+ *
13
+ * - `contains`, `not`, `propertyNames`, or `dependentSchemas` whose subschema
14
+ * uses a form {@link canMatchSubschema} cannot prove inline (a cyclic `$ref`,
15
+ * an unknown keyword). The parser enforces these keywords only for subschemas
16
+ * it can match exactly; anything else would be silently ignored.
17
+ *
18
+ * - A node whose enforcement rests on the whole-schema backstop (see
19
+ * {@link backstopReason} — `unevaluatedProperties` / `unevaluatedItems`, a
20
+ * `$ref` nothing delegates, a type-less constraint) when the matcher cannot
21
+ * build that backstop.
22
+ *
23
+ * Everything the matcher *can* prove is enforced rather than refused, which is
24
+ * why `unevaluated*` and `$ref`-carrying subschemas no longer fail outright.
25
+ *
26
+ * This runs for strict generation only — coercing parsers are documented to be
27
+ * permissive (they repair rather than reject), so ignoring these keywords there
28
+ * is the contract, not a lie.
29
+ */
30
+ export declare const assertNoUnsupportedKeywords: (schema: JSONSchema, typeName: string, context?: StrictAssertionContext) => void;
@@ -0,0 +1,87 @@
1
+ import { isSchemaObject } from "@amritk/helpers/schema-guards";
2
+ import { backstopReason, backstopSchema } from "./generate-strict-assertion.js";
3
+ import { canMatchSubschema } from "./subschema-match.js";
4
+ const SUBSCHEMA_KEYS = [
5
+ "items",
6
+ "prefixItems",
7
+ "additionalItems",
8
+ "contains",
9
+ "additionalProperties",
10
+ "propertyNames",
11
+ "unevaluatedProperties",
12
+ "unevaluatedItems",
13
+ "not",
14
+ "if",
15
+ "then",
16
+ "else",
17
+ "allOf",
18
+ "anyOf",
19
+ "oneOf",
20
+ "contentSchema"
21
+ ];
22
+ const SUBSCHEMA_MAP_KEYS = [
23
+ "properties",
24
+ "patternProperties",
25
+ "dependentSchemas",
26
+ "$defs",
27
+ "definitions"
28
+ ];
29
+ const assertNoUnsupportedKeywords = (schema, typeName, context = {}) => {
30
+ const rootSchema = context.rootSchema ?? (isSchemaObject(schema) ? schema : void 0);
31
+ const provable = (sub) => canMatchSubschema(sub, rootSchema);
32
+ const fail = (keyword, detail) => {
33
+ throw new Error(`[${typeName}] unsupported keyword "${keyword}": the strict parser generator ${detail}, and would silently accept documents the schema rejects. Generate a coercing (non-strict) parser, validate this schema with a validate mode instead of parseStrict, or remove the keyword.`);
34
+ };
35
+ const visit = (node) => {
36
+ if (typeof node !== "object" || node === null || Array.isArray(node))
37
+ return;
38
+ const record = node;
39
+ if ("contains" in record && !provable(record["contains"])) {
40
+ fail("contains", "cannot prove its subschema inline");
41
+ }
42
+ if ("not" in record && !provable(record["not"])) {
43
+ fail("not", "cannot prove its subschema inline");
44
+ }
45
+ if ("propertyNames" in record && isSchemaObject(record["propertyNames"])) {
46
+ if (!provable(record["propertyNames"])) {
47
+ fail("propertyNames", "cannot prove its subschema inline");
48
+ }
49
+ }
50
+ const dependentSchemas = record["dependentSchemas"];
51
+ if (typeof dependentSchemas === "object" && dependentSchemas !== null && !Array.isArray(dependentSchemas)) {
52
+ for (const sub of Object.values(dependentSchemas)) {
53
+ if (typeof sub === "boolean")
54
+ continue;
55
+ if (!provable(sub)) {
56
+ fail("dependentSchemas", "cannot prove one of its subschemas inline");
57
+ }
58
+ }
59
+ }
60
+ const reason = backstopReason(node, context);
61
+ if (reason !== null && !provable(backstopSchema(node, reason))) {
62
+ fail(reason, "cannot prove the schema it appears in inline");
63
+ }
64
+ for (const keyword of SUBSCHEMA_KEYS) {
65
+ const value = record[keyword];
66
+ if (value === void 0)
67
+ continue;
68
+ if (Array.isArray(value)) {
69
+ for (const item of value)
70
+ visit(item);
71
+ } else {
72
+ visit(value);
73
+ }
74
+ }
75
+ for (const keyword of SUBSCHEMA_MAP_KEYS) {
76
+ const value = record[keyword];
77
+ if (typeof value !== "object" || value === null || Array.isArray(value))
78
+ continue;
79
+ for (const sub of Object.values(value))
80
+ visit(sub);
81
+ }
82
+ };
83
+ visit(schema);
84
+ };
85
+ export {
86
+ assertNoUnsupportedKeywords
87
+ };