@jarenjs/emit 0.34.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,156 @@
1
+ /**
2
+ * The stylesheet. Every rule dispatches on a node's `kind`, so the model's
3
+ * vocabulary is the only coupling between the two stages.
4
+ */
5
+ export declare const TYPESCRIPT_STYLESHEET: {
6
+ $jtlt: string;
7
+ output: string;
8
+ rules: ({
9
+ match: string;
10
+ body: {
11
+ $apply: string[];
12
+ }[][];
13
+ priority?: undefined;
14
+ mode?: undefined;
15
+ } | {
16
+ match: {
17
+ schema: {
18
+ type: string;
19
+ properties: {
20
+ kind: {
21
+ const: string;
22
+ };
23
+ type: {
24
+ properties: {
25
+ kind: {
26
+ const: string;
27
+ };
28
+ };
29
+ };
30
+ };
31
+ required: string[];
32
+ };
33
+ };
34
+ mode: string;
35
+ priority: number;
36
+ body: (string | {
37
+ $apply: string[];
38
+ }[] | {
39
+ $raw: string;
40
+ })[];
41
+ } | {
42
+ match: {
43
+ schema: {
44
+ type: string;
45
+ properties: {
46
+ kind: {
47
+ const: any;
48
+ };
49
+ };
50
+ required: string[];
51
+ };
52
+ };
53
+ mode: string;
54
+ priority: number;
55
+ body: (string | {
56
+ $apply: string[];
57
+ }[] | {
58
+ $raw: string;
59
+ })[];
60
+ } | {
61
+ priority?: undefined;
62
+ mode: string;
63
+ body: (string | {
64
+ $apply: string[];
65
+ }[] | {
66
+ $raw: string;
67
+ $if?: undefined;
68
+ } | {
69
+ $raw?: undefined;
70
+ $if: (string | {
71
+ $not: string;
72
+ })[];
73
+ })[];
74
+ match?: undefined;
75
+ } | {
76
+ priority?: undefined;
77
+ match: {
78
+ schema: {
79
+ type: string;
80
+ properties: {
81
+ kind: {
82
+ const: any;
83
+ };
84
+ };
85
+ required: string[];
86
+ };
87
+ };
88
+ mode: string;
89
+ body: {
90
+ $raw: string;
91
+ }[];
92
+ } | {
93
+ priority?: undefined;
94
+ match: {
95
+ schema: {
96
+ type: string;
97
+ properties: {
98
+ kind: {
99
+ const: any;
100
+ };
101
+ };
102
+ required: string[];
103
+ };
104
+ };
105
+ mode: string;
106
+ body: {
107
+ $json: string;
108
+ }[];
109
+ } | {
110
+ priority?: undefined;
111
+ match: {
112
+ schema: {
113
+ type: string;
114
+ properties: {
115
+ kind: {
116
+ const: any;
117
+ };
118
+ };
119
+ required: string[];
120
+ };
121
+ };
122
+ mode: string;
123
+ body: (string | {
124
+ $apply: string[];
125
+ }[])[];
126
+ })[];
127
+ };
128
+ export type EmitModel = import('./model.js').EmitModel;
129
+ export type EmitModelOptions = import('./model.js').EmitModelOptions;
130
+ export type RenderTypeScriptOptions = {
131
+ /**
132
+ * - Emit the do-not-edit header
133
+ */
134
+ banner?: boolean;
135
+ };
136
+ /**
137
+ * Render a type model as TypeScript declarations.
138
+ * @param {EmitModel} model - A type model from `compileEmitModel`
139
+ * @param {RenderTypeScriptOptions} [options]
140
+ * @returns {string} TypeScript source
141
+ */
142
+ export declare function renderTypeScript(model: EmitModel, options?: RenderTypeScriptOptions): string;
143
+ /**
144
+ * Compile a JSON Schema straight to TypeScript declarations — the one-call
145
+ * form of {@link compileEmitModel} followed by {@link renderTypeScript}.
146
+ * @param {object|boolean} schema - The schema to emit
147
+ * @param {EmitModelOptions & RenderTypeScriptOptions} [options] - Model and
148
+ * rendering options — `normalize` and `variantSuffix` included, so the
149
+ * programmatic route can do everything the CLI flags can
150
+ * @returns {string} TypeScript source
151
+ * @example
152
+ * emitTypeScript({ type: 'object', properties: { id: { type: 'string' } } },
153
+ * { name: 'User' });
154
+ * // export interface User { id?: string; }
155
+ */
156
+ export declare function emitTypeScript(schema: object | boolean, options?: EmitModelOptions & RenderTypeScriptOptions): string;
@@ -0,0 +1,273 @@
1
+ # The Jaren emit type model — format 0.1
2
+
3
+ The contract between a schema analysis and an artifact emitter.
4
+
5
+ This document is normative for producers and consumers of the model; the
6
+ machine-checkable form is
7
+ [`jaren-emit-model.schema.json`](../schemas/jaren-emit-model.schema.json), and
8
+ the package's test suite validates every model it produces against it.
9
+
10
+ ## 1. Why the model is a separate, published stage
11
+
12
+ A JSON Schema graph cannot be walked top-to-bottom by a template:
13
+
14
+ - `$ref` points sideways, and may point in a cycle;
15
+ - a subschema may be anonymous, while a declaration needs a name;
16
+ - `allOf` is intersection, `anyOf`/`oneOf` are unions, and `if`/`then` is
17
+ neither;
18
+ - roughly half the vocabulary constrains a value without narrowing its type.
19
+
20
+ The model is the result of resolving all four. It is a **flat, ordered list of
21
+ named declarations** in which every reference is by name — so a cyclic schema
22
+ produces an acyclic model — and every constraint that a type cannot carry is
23
+ recorded rather than dropped.
24
+
25
+ Publishing it, rather than keeping it as an emitter's private state, is what
26
+ makes a third-party emitter a first-class citizen: the bundled TypeScript and
27
+ Markdown emitters read exactly this and nothing else.
28
+
29
+ ## 2. The document
30
+
31
+ ```json
32
+ { "$emit": "0.1",
33
+ "source": "user.json",
34
+ "root": "User",
35
+ "declarations": [] }
36
+ ```
37
+
38
+ - `$emit` (REQUIRED) — the format version, the string `"0.1"`.
39
+ - `declarations` (REQUIRED) — the declarations, in **emission order**.
40
+ - `source` (OPTIONAL) — where the model came from; emitters may put it in a
41
+ generated banner. `null` when unknown.
42
+ - `root` (OPTIONAL) — the declaration corresponding to the source schema's
43
+ root. `null` for a bundle with no single root.
44
+
45
+ ## 3. Declarations
46
+
47
+ ```json
48
+ { "kind": "declaration",
49
+ "name": "User",
50
+ "type": { "kind": "object", "members": [] },
51
+ "constraints": [],
52
+ "doc": ["A user"] }
53
+ ```
54
+
55
+ `name` is unique across the document and is a safe identifier: it matches
56
+ `[A-Za-z_$][A-Za-z0-9_$]*`. A producer derives it from the schema's own
57
+ vocabulary where one exists (a `$defs` key, a `$ref`'s last pointer token) and
58
+ otherwise from the path that reached the node. **Names are deterministic**: the
59
+ same input document produces the same names in the same order, because a
60
+ generated artifact that churns between runs cannot be reviewed in a diff.
61
+
62
+ ## 4. Members
63
+
64
+ ```json
65
+ { "kind": "member",
66
+ "name": "id",
67
+ "type": { "kind": "primitive", "primitive": "string" },
68
+ "required": true,
69
+ "default": 0,
70
+ "constraints": [],
71
+ "doc": [] }
72
+ ```
73
+
74
+ `name` is the property name **verbatim** — it is data, not an identifier, and
75
+ an emitter quotes it if its target language requires that. `default` is
76
+ present only when the source schema declares one. Members appear in the order
77
+ the schema declared them.
78
+
79
+ ### 4.1 The extension seam
80
+
81
+ Unknown keywords normally vanish from the model — only the listed
82
+ constraint groups are carried. A producer MAY be told to preserve
83
+ named extension keywords instead:
84
+
85
+ ```js
86
+ compileEmitModel(schema, { extensions: ['x-entity'] })
87
+ ```
88
+
89
+ Every property member whose schema declares a listed keyword gains
90
+
91
+ ```json
92
+ { "kind": "member", "name": "posts",
93
+ "extensions": { "x-entity": { "relation": { "to": "Post" } } }, … }
94
+ ```
95
+
96
+ with the value copied **verbatim and uninterpreted** — the compiler
97
+ learns "keep these keywords", never what they mean. Keywords are read
98
+ from the property schema node itself: a vocabulary that wants to ride
99
+ this seam declares its members inline, not behind `$ref`. `extensions`
100
+ is absent when no listed keyword is present, and the option is absent
101
+ by default, so models without it are byte-identical to before.
102
+
103
+ This is the contract for vocabulary-aware artifacts: a downstream
104
+ package post-processes the MODEL DOCUMENT (replace member types,
105
+ adjust `required`, add declarations) and hands the result back to any
106
+ renderer. `@jarenjs/db` renders entity-aware TypeScript from
107
+ `x-entity` this way, and `x-form` is the obvious second customer —
108
+ the seam serves any vocabulary, which is why it lives here and not in
109
+ either consumer.
110
+
111
+ ## 5. Type references
112
+
113
+ Every type reference carries a `kind`, so an emitter dispatches on shape
114
+ rather than on position. That is what lets a stylesheet match with a schema
115
+ match and stay correct under re-dispatch.
116
+
117
+ | `kind` | members | meaning |
118
+ |---|---|---|
119
+ | `unknown` | — | nothing is known; the honest top type |
120
+ | `never` | — | nothing satisfies this (`false` schema) |
121
+ | `primitive` | `primitive` | one of `string`, `number`, `boolean`, `null` |
122
+ | `literal` | `value` | exactly this JSON value |
123
+ | `ref` | `ref` | the declaration with that `name` |
124
+ | `array` | `items` | a homogeneous list |
125
+ | `tuple` | `items`, `rest?` | positional items, optionally followed by a rest type |
126
+ | `optional` | `item` | a tuple position that may be absent; only valid inside `tuple.items`, after every non-optional position |
127
+ | `record` | `value` | a map from string keys to `value` |
128
+ | `object` | `members`, `index?` | declared members, plus an optional index signature |
129
+ | `union` | `options` (≥ 2) | any one of |
130
+ | `intersection` | `parts` (≥ 2) | all of |
131
+
132
+ A producer MUST collapse the degenerate forms: a union or intersection of one
133
+ is that one type, and a union containing `unknown` is `unknown`. A consumer is
134
+ therefore entitled to assume `options` and `parts` have at least two entries.
135
+ A tuple with no positional items MUST also collapse: with a `rest` type it is
136
+ an `array` of that type, and without one it is the empty tuple — a consumer
137
+ never has to print a rest with nothing in front of it.
138
+
139
+ **Tuples mirror what the schema enforces, not what it suggests.** JSON Schema
140
+ `prefixItems` constrains the positions that exist; `minItems` says how many
141
+ must; an omitted rest schema leaves the array open. A producer therefore
142
+ marks only the first `minItems` positions non-optional and emits an `unknown`
143
+ rest for an omitted one — a closed, all-required tuple is emitted only when
144
+ the schema actually closes it.
145
+
146
+ **`index` covers the declared members.** When an object has both members and
147
+ an index signature, the index type is widened to include every member's type,
148
+ because TypeScript — and any other target with the same rule — rejects an
149
+ index signature that does not.
150
+
151
+ ## 6. Constraints and documentation
152
+
153
+ ```json
154
+ { "constraints": [{ "keyword": "minLength", "value": 3 }],
155
+ "doc": ["Display name",
156
+ "Schema constraints this type cannot express: minLength=3"] }
157
+ ```
158
+
159
+ `constraints` is the **structured** record of everything the source schema
160
+ asserted that the type does not enforce. `doc` is the same information already
161
+ flattened to lines, together with the schema's `description`, so an emitter
162
+ prints lines rather than deciding what belongs in a comment.
163
+
164
+ Both are always present; an empty array means there is nothing to say, and an
165
+ emitter MUST then produce no comment at all. That absence is deliberate: it
166
+ lets a template express "only when present" by dispatching a path that yields
167
+ the empty sequence, without a conditional.
168
+
169
+ **A producer MUST NOT drop a constraint silently.** The whole reason a
170
+ generated type is trustworthy is that what it cannot say, it says it cannot
171
+ say.
172
+
173
+ ## 7. Accepted and normalized variants
174
+
175
+ `@jarenjs/validate/normalize` makes a contract's input and output shapes
176
+ differ: a defaulted member is optional for the caller and present afterwards,
177
+ and a coerced member accepts its transport form on the way in. A model
178
+ compiled with normalization options carries **both**, so a typed contract
179
+ boundary can name them separately.
180
+
181
+ ```json
182
+ { "$emit": "0.1", "variants": true, "declarations": [
183
+ { "kind": "declaration", "name": "Config", "variant": "normalized", "…": "…" },
184
+ { "kind": "declaration", "name": "ConfigInput", "variant": "accepted",
185
+ "variantOf": "Config", "…": "…" } ] }
186
+ ```
187
+
188
+ - `variants` on the document is `true` exactly when variant pairs were
189
+ derived. Without normalization options there are no `variant` members at
190
+ all, and each declaration is simply the schema's shape.
191
+ - The **normalized** declaration keeps the plain name. It is the shape *after*
192
+ normalizing, which is what the rest of a program handles.
193
+ - The **accepted** declaration takes a suffix (`Input` by default) and names
194
+ its counterpart in `variantOf`.
195
+
196
+ **A twin is emitted only when the type actually differs.** A producer computes
197
+ that bottom-up — a type differs if anything it contains differs — so a schema
198
+ with one defaulted field does not double every declaration in the document.
199
+ Everything unaffected is referenced by its single shared name from both sides.
200
+
201
+ **Two normalizations produce a difference, and only two.** `useDefaults` moves
202
+ a member from optional to required across the boundary. `coerceTypes` widens
203
+ the accepted side to the types the normalizer will convert *from*. `trimStrings`
204
+ is string-to-string and `removeAdditional` removes members no type declared, so
205
+ neither justifies a second declaration.
206
+
207
+ A producer MUST derive the accepted side from the **same** switch resolution
208
+ the normalizer uses, including predicate options. A variant that disagrees
209
+ with the normalizer is worse than no variant: it is a type that certifies an
210
+ input the normalizer will not accept.
211
+
212
+ The same rule extends to *where* normalization runs. `compileNormalizer`
213
+ deliberately does not descend `anyOf`/`oneOf` — which branch applies is only
214
+ known after validating — so inside a union branch no default materializes and
215
+ no coercion applies. A producer MUST NOT let variant semantics leak into
216
+ union branches: a branch references the schema's **as-declared** reading.
217
+ When a referenced type differs under normalization, that reading is its own
218
+ `Plain`-suffixed declaration, shared by both sides of the pair; when it does
219
+ not differ, the branch shares the single plain-named declaration.
220
+
221
+ Three consequences worth stating because each was once wrong:
222
+
223
+ - a **`required` member with an enabled default** is still optional on the
224
+ accepted side — the normalizer materializes it before validation runs;
225
+ - a **`const`/`enum` with an enabled coercion** widens its accepted side by
226
+ the source primitives that can actually reach a member of the literal set
227
+ (an integer enum admits `string`; a string enum of words admits nothing
228
+ extra, because no number ever becomes `"admin"`);
229
+ - coercion widens only nodes with a **single string-valued `type`**, because
230
+ that is the only place `coerceToType` runs.
231
+
232
+ ## 8. Determinism
233
+
234
+ Two compilations of the same input MUST produce byte-identical models.
235
+ Concretely, a producer:
236
+
237
+ - keeps schema declaration order for members and `$defs`;
238
+ - keeps discovery order for declarations;
239
+ - derives names from the document, never from a counter that depends on
240
+ traversal timing;
241
+ - does not serialize a `Set` or `Map` whose order depends on insertion history
242
+ across merges.
243
+
244
+ ## 9. What the format deliberately does not model
245
+
246
+ - **`if`/`then`/`else` and `not`.** Neither has a sound type-level reading —
247
+ the first is a conditional type in principle and unreadable in practice, the
248
+ second has no equivalent at all. A producer ignores them for the type and
249
+ **records them as dropped constraints** (§6), rather than inventing a union
250
+ that would be wrong in one direction or the other. The same recording
251
+ applies to the other silently-widening keywords: integer-ness (`type:
252
+ "integer"` emits as `number`), `dependentSchemas`/`dependencies`,
253
+ constraining `unevaluated*` values, and the key restrictions of
254
+ `patternProperties` (the index signature carries their value types, but no
255
+ type restricts which keys a pattern admits).
256
+ - **Type inference from applicators.** `properties` or `items` on a node with
257
+ no `type` does not make it an object or an array — the validator accepts a
258
+ primitive without reading either. A producer emits the described container
259
+ shape as one union arm and the remaining JSON kinds beside it.
260
+ - **Cross-document `$ref`.** A model compiles the document it was given.
261
+ Following a ref into another document would mean owning a resolution scope,
262
+ which is `@jarenjs/validate`'s job, not this format's. Within the document,
263
+ the resolved forms are exactly the ones `compileNormalizer` resolves — `#`,
264
+ `#/pointer` and plain `#anchor` (outside embedded `$id` resources) — and a
265
+ `$ref`'s siblings compose with its target as an intersection, per 2019-09+.
266
+ That composition is unconditional, like the normalizer's: a draft-07
267
+ document in which a validator lets `$ref` shadow its siblings should not
268
+ put constraining siblings there — they are dead keywords to that validator,
269
+ and this producer takes them at their word.
270
+ - **`removeAdditional` and `trimStrings` as type differences.** Neither
271
+ changes a *declared* type: trimming is string-to-string, and stripping
272
+ removes members the type never declared. Only `useDefaults` and
273
+ `coerceTypes` earn a variant (§7).
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@jarenjs/emit",
3
+ "private": false,
4
+ "version": "0.34.0",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./dist/types/index.d.ts",
8
+ "sideEffects": false,
9
+ "bin": {
10
+ "jaren-emit": "./src/cli.js"
11
+ },
12
+ "files": [
13
+ "dist/types/",
14
+ "src/",
15
+ "schemas/",
16
+ "docs/",
17
+ "ARCHITECTURE.md"
18
+ ],
19
+ "description": "Build-time artifacts from JSON documents: JSON Schema to TypeScript declarations and beyond, via JTLT stylesheets",
20
+ "author": "joham",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/jklarenbeek/jarenjs.git",
24
+ "directory": "packages/emit"
25
+ },
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=24"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "registry": "https://registry.npmjs.org/"
33
+ },
34
+ "keywords": [
35
+ "jaren",
36
+ "json-schema",
37
+ "typescript",
38
+ "codegen",
39
+ "code-generation",
40
+ "types",
41
+ "dts",
42
+ "json-schema-to-typescript",
43
+ "jtlt"
44
+ ],
45
+ "exports": {
46
+ ".": {
47
+ "types": "./dist/types/index.d.ts",
48
+ "default": "./src/index.js"
49
+ },
50
+ "./model": {
51
+ "types": "./dist/types/model.d.ts",
52
+ "default": "./src/model.js"
53
+ },
54
+ "./typescript": {
55
+ "types": "./dist/types/typescript.d.ts",
56
+ "default": "./src/typescript.js"
57
+ },
58
+ "./markdown": {
59
+ "types": "./dist/types/markdown.d.ts",
60
+ "default": "./src/markdown.js"
61
+ },
62
+ "./schemas/*": "./schemas/*",
63
+ "./package.json": "./package.json"
64
+ },
65
+ "scripts": {
66
+ "build": "npm run build:types",
67
+ "build:types": "tsc -p tsconfig.json",
68
+ "prepack": "npm run build:types"
69
+ },
70
+ "dependencies": {
71
+ "@jarenjs/core": "^0.34.0",
72
+ "@jarenjs/json": "^0.34.0",
73
+ "@jarenjs/validate": "^0.34.0"
74
+ }
75
+ }
@@ -0,0 +1,171 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/jklarenbeek/jarenjs/schemas/jaren-emit-model.schema.json",
4
+ "title": "Jaren emit type model",
5
+ "description": "The intermediate between a JSON Schema graph and a generated artifact. Published so that a third-party emitter targets the same contract the bundled ones do.",
6
+ "type": "object",
7
+ "required": ["$emit", "declarations"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$emit": {
11
+ "description": "The model format version.",
12
+ "const": "0.1"
13
+ },
14
+ "source": {
15
+ "description": "Where the model came from, recorded for the generated banner.",
16
+ "type": ["string", "null"]
17
+ },
18
+ "root": {
19
+ "description": "The declaration name that corresponds to the schema's root.",
20
+ "type": ["string", "null"]
21
+ },
22
+ "variants": {
23
+ "description": "Present and true when the model carries accepted/normalized variant pairs, i.e. it was compiled with normalization options.",
24
+ "const": true
25
+ },
26
+ "declarations": {
27
+ "description": "Named declarations in emission order. Every reference is by name, so the list is flat even when the source schema was cyclic.",
28
+ "type": "array",
29
+ "items": { "$ref": "#/$defs/declaration" }
30
+ }
31
+ },
32
+ "$defs": {
33
+ "declaration": {
34
+ "type": "object",
35
+ "required": ["kind", "name", "type", "constraints", "doc"],
36
+ "additionalProperties": false,
37
+ "properties": {
38
+ "kind": { "const": "declaration" },
39
+ "variant": {
40
+ "description": "Which side of normalization this declaration describes. Absent when the model was compiled without normalization options, in which case the single declaration is both.",
41
+ "enum": ["accepted", "normalized"]
42
+ },
43
+ "variantOf": {
44
+ "description": "For an accepted variant, the name of its normalized counterpart.",
45
+ "type": "string"
46
+ },
47
+ "name": {
48
+ "description": "A unique, identifier-safe name.",
49
+ "type": "string",
50
+ "minLength": 1
51
+ },
52
+ "type": { "$ref": "#/$defs/type" },
53
+ "constraints": { "$ref": "#/$defs/constraints" },
54
+ "doc": { "$ref": "#/$defs/doc" }
55
+ }
56
+ },
57
+ "member": {
58
+ "type": "object",
59
+ "required": ["kind", "name", "type", "required", "constraints", "doc"],
60
+ "additionalProperties": false,
61
+ "properties": {
62
+ "kind": { "const": "member" },
63
+ "name": { "type": "string" },
64
+ "type": { "$ref": "#/$defs/type" },
65
+ "required": { "type": "boolean" },
66
+ "default": { "description": "The schema default, when it declares one." },
67
+ "constraints": { "$ref": "#/$defs/constraints" },
68
+ "doc": { "$ref": "#/$defs/doc" }
69
+ }
70
+ },
71
+ "doc": {
72
+ "description": "Documentation lines: the description, then a summary of the constraints the type cannot express. Empty means the node has nothing to say, and an emitter should print no comment at all.",
73
+ "type": "array",
74
+ "items": { "type": "string" }
75
+ },
76
+ "constraints": {
77
+ "description": "Keywords the source schema states that a type cannot carry. Recorded rather than dropped: a reader of the generated artifact must be able to learn that the constraint exists and is not enforced by the type.",
78
+ "type": "array",
79
+ "items": {
80
+ "type": "object",
81
+ "required": ["keyword"],
82
+ "additionalProperties": false,
83
+ "properties": {
84
+ "keyword": { "type": "string" },
85
+ "value": {}
86
+ }
87
+ }
88
+ },
89
+ "type": {
90
+ "description": "A type reference. Every form carries a `kind`, so an emitter dispatches on shape rather than on position.",
91
+ "type": "object",
92
+ "required": ["kind"],
93
+ "properties": {
94
+ "kind": {
95
+ "enum": ["unknown", "never", "primitive", "literal", "ref",
96
+ "array", "tuple", "optional", "record", "union", "intersection", "object"]
97
+ }
98
+ },
99
+ "allOf": [
100
+ {
101
+ "if": { "properties": { "kind": { "const": "primitive" } }, "required": ["kind"] },
102
+ "then": {
103
+ "required": ["primitive"],
104
+ "properties": { "primitive": { "enum": ["string", "number", "boolean", "null"] } }
105
+ }
106
+ },
107
+ {
108
+ "if": { "properties": { "kind": { "const": "literal" } }, "required": ["kind"] },
109
+ "then": { "required": ["value"] }
110
+ },
111
+ {
112
+ "if": { "properties": { "kind": { "const": "ref" } }, "required": ["kind"] },
113
+ "then": { "required": ["ref"], "properties": { "ref": { "type": "string" } } }
114
+ },
115
+ {
116
+ "if": { "properties": { "kind": { "const": "array" } }, "required": ["kind"] },
117
+ "then": { "required": ["items"], "properties": { "items": { "$ref": "#/$defs/type" } } }
118
+ },
119
+ {
120
+ "if": { "properties": { "kind": { "const": "record" } }, "required": ["kind"] },
121
+ "then": { "required": ["value"], "properties": { "value": { "$ref": "#/$defs/type" } } }
122
+ },
123
+ {
124
+ "if": { "properties": { "kind": { "const": "tuple" } }, "required": ["kind"] },
125
+ "then": {
126
+ "required": ["items"],
127
+ "properties": {
128
+ "items": { "type": "array", "items": { "$ref": "#/$defs/type" } },
129
+ "rest": { "$ref": "#/$defs/type" }
130
+ }
131
+ }
132
+ },
133
+ {
134
+ "if": { "properties": { "kind": { "const": "optional" } }, "required": ["kind"] },
135
+ "then": {
136
+ "required": ["item"],
137
+ "properties": { "item": { "$ref": "#/$defs/type" } }
138
+ }
139
+ },
140
+ {
141
+ "if": { "properties": { "kind": { "const": "union" } }, "required": ["kind"] },
142
+ "then": {
143
+ "required": ["options"],
144
+ "properties": {
145
+ "options": { "type": "array", "minItems": 2, "items": { "$ref": "#/$defs/type" } }
146
+ }
147
+ }
148
+ },
149
+ {
150
+ "if": { "properties": { "kind": { "const": "intersection" } }, "required": ["kind"] },
151
+ "then": {
152
+ "required": ["parts"],
153
+ "properties": {
154
+ "parts": { "type": "array", "minItems": 2, "items": { "$ref": "#/$defs/type" } }
155
+ }
156
+ }
157
+ },
158
+ {
159
+ "if": { "properties": { "kind": { "const": "object" } }, "required": ["kind"] },
160
+ "then": {
161
+ "required": ["members"],
162
+ "properties": {
163
+ "members": { "type": "array", "items": { "$ref": "#/$defs/member" } },
164
+ "index": { "$ref": "#/$defs/type" }
165
+ }
166
+ }
167
+ }
168
+ ]
169
+ }
170
+ }
171
+ }