@maroonedog/luq 2.5.0 → 2.6.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 (34) hide show
  1. package/README.md +23 -7
  2. package/dist/chain/slot-type-guard.js +20 -6
  3. package/dist/chain/slot-type-guard.mjs +20 -6
  4. package/dist/compile/resolve-presence.js +36 -5
  5. package/dist/compile/resolve-presence.mjs +36 -5
  6. package/dist/json-schema/assert-supported-dialect.d.ts +37 -0
  7. package/dist/json-schema/assert-supported-dialect.js +113 -0
  8. package/dist/json-schema/assert-supported-dialect.mjs +109 -0
  9. package/dist/json-schema/build-from-schema.d.ts +22 -3
  10. package/dist/json-schema/build-from-schema.js +27 -5
  11. package/dist/json-schema/build-from-schema.mjs +27 -5
  12. package/dist/json-schema/extensions/json-schema/index.d.ts +2 -2
  13. package/dist/json-schema/extensions/json-schema/index.js +3 -1
  14. package/dist/json-schema/extensions/json-schema/index.mjs +1 -1
  15. package/dist/json-schema/extensions/json-schema/json-schema.d.ts +2 -2
  16. package/dist/json-schema/extensions/json-schema/json-schema.js +3 -0
  17. package/dist/json-schema/extensions/json-schema/json-schema.mjs +4 -1
  18. package/dist/json-schema/extensions/json-schema-full-feature/index.d.ts +3 -1
  19. package/dist/json-schema/extensions/json-schema-full-feature/index.js +18 -1
  20. package/dist/json-schema/extensions/json-schema-full-feature/index.mjs +11 -0
  21. package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.d.ts +9 -1
  22. package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.js +9 -2
  23. package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.mjs +9 -2
  24. package/dist/json-schema/index.d.ts +3 -0
  25. package/dist/json-schema/index.js +7 -1
  26. package/dist/json-schema/index.mjs +2 -0
  27. package/dist/json-schema/keyword-map-core.js +3 -1
  28. package/dist/json-schema/keyword-map-core.mjs +3 -1
  29. package/dist/json-schema/keyword-map.js +3 -0
  30. package/dist/json-schema/keyword-map.mjs +3 -0
  31. package/dist/json-schema/unsupported-dialect-error.d.ts +21 -0
  32. package/dist/json-schema/unsupported-dialect-error.js +58 -0
  33. package/dist/json-schema/unsupported-dialect-error.mjs +54 -0
  34. package/package.json +6 -3
package/README.md CHANGED
@@ -42,6 +42,7 @@ compile error, not a rule that quietly never fires.
42
42
  | Descending into a built-in (`"when.getTime"` on a `Date`) | compile error |
43
43
  | A method that does not exist inside an element sub-chain | compile error |
44
44
  | A JSON Schema keyword bound to a chain method that does not exist | compile error |
45
+ | A JSON Schema document declaring 2019-09 or 2020-12 | throws when the validator is built, rather than being read as Draft-07 |
45
46
  | A documented example drifting from the API | fails CI |
46
47
 
47
48
  That matters most when the code calling this library is generated rather than
@@ -136,13 +137,24 @@ const result = orderValidator.validate({ reference: "ab", quantity: 0 });
136
137
  if (!result.valid) {
137
138
  for (const issue of result.issues) {
138
139
  // issue.path "reference" — where, with array indices filled in
139
- // issue.code "stringMin" — which rule, stable across messages
140
+ // issue.code "stringMin" — which rule; the vocabulary is pinned
140
141
  // issue.message — the text, overridable per call
141
142
  // issue.severity "error" — only "error" makes the value invalid
142
143
  }
143
144
  }
144
145
  ```
145
146
 
147
+ `issue.code` is the machine-readable half: a plugin's own name by default,
148
+ overridden per rule with `{ code }`. The set is not a convention. The codes the
149
+ library reports are enumerated in `config/issue-code.lock.json`, derived from
150
+ the source by `npm run generate:issue-codes` and checked by
151
+ `npm run check:issue-code-lock`, so renaming one is a diff in a committed file
152
+ and a deliberate act rather than a silent break. The lock records who reports
153
+ each code, because some codes are shared on purpose — a missing root and a
154
+ missing field both report `required`. Codes a gate carries (`skip`,
155
+ `validateIf`) sit in a separate list: they are accepted from a caller but no
156
+ issue can ever carry one.
157
+
146
158
  `.v(path, chain)` declares rules for one field. A path you do not declare is
147
159
  not validated, not required and not read, so covering a type partly is a normal
148
160
  state rather than a half-finished one.
@@ -295,7 +307,7 @@ const forStringMin = PLUGIN_MANIFEST.filter((entry) =>
295
307
  | [Getting started](https://luq.dev/docs/getting-started) | the builder, defaults, `normalize`, reading a result |
296
308
  | [Core concepts](https://luq.dev/docs/core-concepts) | field paths, slots, presence, transforms |
297
309
  | [Plugins](https://luq.dev/plugins) | every subpath, symbol, chain method and slot |
298
- | [JSON Schema](https://luq.dev/json-schema) | reading a document in, writing one back out, measured Draft-07 conformance |
310
+ | [JSON Schema](https://luq.dev/json-schema) | reading a Draft-07 document in, writing one back out, measured conformance, and why a newer-dialect document is refused |
299
311
  | [Standard Schema](https://luq.dev/standard-schema) | tRPC, TanStack Form, Hono, react-hook-form — and what does not cross that boundary |
300
312
  | [Benchmarks](https://luq.dev/benchmarks) | bundle size and throughput, with the method |
301
313
  | [Luq or zod?](https://luq.dev/luq-or-zod) | when schema-first is the better answer |
@@ -311,6 +323,8 @@ Breaking changes happen in a major and nowhere else, an API being removed is
311
323
  deprecated one major ahead, and each major ships with the codemod needed to
312
324
  cross it.
313
325
 
326
+ - **[CHANGELOG.md](https://github.com/maroonedog/luq/blob/master/CHANGELOG.md)** — every released version, what a caller
327
+ sees change in each, and which ones need reading before you take them
314
328
  - **[CONTRIBUTING.md](https://github.com/maroonedog/luq/blob/master/CONTRIBUTING.md)** — `npm run verify` is the whole
315
329
  contract; the gates and what each one refuses
316
330
  - **[SECURITY.md](https://github.com/maroonedog/luq/blob/master/SECURITY.md)** — reporting, zero runtime dependencies, the
@@ -320,11 +334,13 @@ cross it.
320
334
 
321
335
  ## About the "universal platform" goal
322
336
 
323
- 1.x described a `.luq` DSL that would generate validators for other languages,
324
- against dated milestones. Those dates have passed and none of it shipped, so the
325
- plan has been withdrawn rather than moved: no part of it is in this package, and
326
- this release makes no claim about when any of it will exist. What is in the box
327
- is the TypeScript validation library described above.
337
+ The first published line `0.1.0-alpha` through `0.1.2-alpha`, which the
338
+ migration guide and the docs site both call "1.x" although no `1.x` was ever
339
+ published described a `.luq` DSL that would generate validators for other
340
+ languages, against dated milestones. Those dates have passed and none of it
341
+ shipped, so the plan has been withdrawn rather than moved: no part of it is in
342
+ this package, and this release makes no claim about when any of it will exist.
343
+ What is in the box is the TypeScript validation library described above.
328
344
 
329
345
  ## License
330
346
 
@@ -3,16 +3,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.slotTypeGuard = slotTypeGuard;
4
4
  const types_1 = require("../types");
5
5
  const create_rule_1 = require("../plugin-kit/create-rule");
6
+ /**
7
+ * Each code is SPELLED here rather than interpolated from the slot name.
8
+ *
9
+ * `${slot}Type` produced the same six strings, and produced them nowhere a
10
+ * reader could find them: config/issue-code.lock.json enumerates every code
11
+ * the library can report, and a code that only exists once a template has been
12
+ * evaluated cannot be enumerated, so these six were missing from the published
13
+ * vocabulary while being among the most commonly reported codes there are.
14
+ */
6
15
  const SLOT_TYPES = Object.freeze({
7
- string: { accepts: types_1.isString, noun: "a string" },
8
- number: { accepts: types_1.isNumber, noun: "a number" },
16
+ string: { accepts: types_1.isString, code: "stringType", noun: "a string" },
17
+ number: { accepts: types_1.isNumber, code: "numberType", noun: "a number" },
9
18
  boolean: {
10
19
  accepts: (value) => typeof value === "boolean",
20
+ code: "booleanType",
11
21
  noun: "a boolean",
12
22
  },
13
- date: { accepts: (value) => value instanceof Date, noun: "a Date" },
14
- array: { accepts: types_1.isArray, noun: "an array" },
15
- object: { accepts: types_1.isPlainObject, noun: "an object" },
23
+ date: {
24
+ accepts: (value) => value instanceof Date,
25
+ code: "dateType",
26
+ noun: "a Date",
27
+ },
28
+ array: { accepts: types_1.isArray, code: "arrayType", noun: "an array" },
29
+ object: { accepts: types_1.isPlainObject, code: "objectType", noun: "an object" },
16
30
  });
17
31
  /**
18
32
  * The rule `b.<slot>` starts its chain with, or null for a slot that claims
@@ -26,7 +40,7 @@ function slotTypeGuard(slot, severity) {
26
40
  if (slotType === undefined)
27
41
  return null;
28
42
  return (0, create_rule_1.check)({
29
- code: `${slot}Type`,
43
+ code: slotType.code,
30
44
  severity,
31
45
  run: (value) => value === undefined || value === null || slotType.accepts(value)
32
46
  ? types_1.PASS
@@ -1,15 +1,29 @@
1
1
  import { PASS, fail, isArray, isNumber, isPlainObject, isString, } from "../types/index.mjs";
2
2
  import { check } from "../plugin-kit/create-rule.mjs";
3
+ /**
4
+ * Each code is SPELLED here rather than interpolated from the slot name.
5
+ *
6
+ * `${slot}Type` produced the same six strings, and produced them nowhere a
7
+ * reader could find them: config/issue-code.lock.json enumerates every code
8
+ * the library can report, and a code that only exists once a template has been
9
+ * evaluated cannot be enumerated, so these six were missing from the published
10
+ * vocabulary while being among the most commonly reported codes there are.
11
+ */
3
12
  const SLOT_TYPES = Object.freeze({
4
- string: { accepts: isString, noun: "a string" },
5
- number: { accepts: isNumber, noun: "a number" },
13
+ string: { accepts: isString, code: "stringType", noun: "a string" },
14
+ number: { accepts: isNumber, code: "numberType", noun: "a number" },
6
15
  boolean: {
7
16
  accepts: (value) => typeof value === "boolean",
17
+ code: "booleanType",
8
18
  noun: "a boolean",
9
19
  },
10
- date: { accepts: (value) => value instanceof Date, noun: "a Date" },
11
- array: { accepts: isArray, noun: "an array" },
12
- object: { accepts: isPlainObject, noun: "an object" },
20
+ date: {
21
+ accepts: (value) => value instanceof Date,
22
+ code: "dateType",
23
+ noun: "a Date",
24
+ },
25
+ array: { accepts: isArray, code: "arrayType", noun: "an array" },
26
+ object: { accepts: isPlainObject, code: "objectType", noun: "an object" },
13
27
  });
14
28
  /**
15
29
  * The rule `b.<slot>` starts its chain with, or null for a slot that claims
@@ -23,7 +37,7 @@ export function slotTypeGuard(slot, severity) {
23
37
  if (slotType === undefined)
24
38
  return null;
25
39
  return check({
26
- code: `${slot}Type`,
40
+ code: slotType.code,
27
41
  severity,
28
42
  run: (value) => value === undefined || value === null || slotType.accepts(value)
29
43
  ? PASS
@@ -38,13 +38,43 @@ function countRestrictions(rule) {
38
38
  (rule.allowNull ? 0 : 1) +
39
39
  (rule.emptyStringIsMissing ? 1 : 0));
40
40
  }
41
+ /**
42
+ * Which of two equally restrictive rules is reported, decided on WHAT each one
43
+ * forbids: undefined first, then null, then the empty string.
44
+ *
45
+ * The order is declared here rather than falling out of something else. It
46
+ * runs from the broadest absence to the narrowest — a field that is not there
47
+ * at all, a field that is there holding null, a field holding "" — so the
48
+ * identity a caller sees names the largest thing that was missing.
49
+ *
50
+ * Nothing in this comparison reads a code. A code is a published name, pinned
51
+ * in config/issue-code.lock.json, and re-spelling one must not move which rule
52
+ * a failure is reported under; comparing the two codes made exactly that
53
+ * happen, silently, from a one-word rename.
54
+ *
55
+ * Two rules forbidding the identical three things are indistinguishable to
56
+ * every reader of the policy, so neither outranks the other and the one
57
+ * declared first stays. That is the only case where the order the methods were
58
+ * typed in is observable at all.
59
+ */
60
+ function outranksOnRestriction(rule, incumbent) {
61
+ if (rule.allowUndefined !== incumbent.allowUndefined) {
62
+ return !rule.allowUndefined;
63
+ }
64
+ if (rule.allowNull !== incumbent.allowNull)
65
+ return !rule.allowNull;
66
+ if (rule.emptyStringIsMissing !== incumbent.emptyStringIsMissing) {
67
+ return rule.emptyStringIsMissing;
68
+ }
69
+ return false;
70
+ }
41
71
  /**
42
72
  * The identity a failure is reported under: the rule that forbids the most,
43
- * ties broken by the lexicographically smaller code.
73
+ * ties broken by which absence it forbids.
44
74
  *
45
- * Tie-breaking on the CODE rather than on declaration order is what keeps the
46
- * whole policy — not merely its three flags — independent of the order the
47
- * rules arrived in. `.optional().nullable()` and `.nullable().optional()` are
75
+ * Both halves of the decision read only the three presence flags, so the whole
76
+ * policy — not merely its flags — is independent of the order the rules
77
+ * arrived in. `.optional().nullable()` and `.nullable().optional()` are
48
78
  * deep-equal, which is the property the tests assert.
49
79
  */
50
80
  function selectStrictestPresence(rules) {
@@ -55,7 +85,8 @@ function selectStrictestPresence(rules) {
55
85
  continue;
56
86
  }
57
87
  const difference = countRestrictions(rule) - countRestrictions(strictest);
58
- if (difference > 0 || (difference === 0 && rule.code < strictest.code)) {
88
+ if (difference > 0 ||
89
+ (difference === 0 && outranksOnRestriction(rule, strictest))) {
59
90
  strictest = rule;
60
91
  }
61
92
  }
@@ -34,13 +34,43 @@ function countRestrictions(rule) {
34
34
  (rule.allowNull ? 0 : 1) +
35
35
  (rule.emptyStringIsMissing ? 1 : 0));
36
36
  }
37
+ /**
38
+ * Which of two equally restrictive rules is reported, decided on WHAT each one
39
+ * forbids: undefined first, then null, then the empty string.
40
+ *
41
+ * The order is declared here rather than falling out of something else. It
42
+ * runs from the broadest absence to the narrowest — a field that is not there
43
+ * at all, a field that is there holding null, a field holding "" — so the
44
+ * identity a caller sees names the largest thing that was missing.
45
+ *
46
+ * Nothing in this comparison reads a code. A code is a published name, pinned
47
+ * in config/issue-code.lock.json, and re-spelling one must not move which rule
48
+ * a failure is reported under; comparing the two codes made exactly that
49
+ * happen, silently, from a one-word rename.
50
+ *
51
+ * Two rules forbidding the identical three things are indistinguishable to
52
+ * every reader of the policy, so neither outranks the other and the one
53
+ * declared first stays. That is the only case where the order the methods were
54
+ * typed in is observable at all.
55
+ */
56
+ function outranksOnRestriction(rule, incumbent) {
57
+ if (rule.allowUndefined !== incumbent.allowUndefined) {
58
+ return !rule.allowUndefined;
59
+ }
60
+ if (rule.allowNull !== incumbent.allowNull)
61
+ return !rule.allowNull;
62
+ if (rule.emptyStringIsMissing !== incumbent.emptyStringIsMissing) {
63
+ return rule.emptyStringIsMissing;
64
+ }
65
+ return false;
66
+ }
37
67
  /**
38
68
  * The identity a failure is reported under: the rule that forbids the most,
39
- * ties broken by the lexicographically smaller code.
69
+ * ties broken by which absence it forbids.
40
70
  *
41
- * Tie-breaking on the CODE rather than on declaration order is what keeps the
42
- * whole policy — not merely its three flags — independent of the order the
43
- * rules arrived in. `.optional().nullable()` and `.nullable().optional()` are
71
+ * Both halves of the decision read only the three presence flags, so the whole
72
+ * policy — not merely its flags — is independent of the order the rules
73
+ * arrived in. `.optional().nullable()` and `.nullable().optional()` are
44
74
  * deep-equal, which is the property the tests assert.
45
75
  */
46
76
  function selectStrictestPresence(rules) {
@@ -51,7 +81,8 @@ function selectStrictestPresence(rules) {
51
81
  continue;
52
82
  }
53
83
  const difference = countRestrictions(rule) - countRestrictions(strictest);
54
- if (difference > 0 || (difference === 0 && rule.code < strictest.code)) {
84
+ if (difference > 0 ||
85
+ (difference === 0 && outranksOnRestriction(rule, strictest))) {
55
86
  strictest = rule;
56
87
  }
57
88
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * What the caller may say about the dialect. `JsonSchemaOptions` extends this,
3
+ * so the chain method and the conversion function offer the same word.
4
+ */
5
+ export interface DialectOptions {
6
+ /**
7
+ * Read the document under Draft-07 rules whatever its `$schema` declares.
8
+ *
9
+ * The reading it buys is Draft-07's, exactly: `$ref` replaces the node it
10
+ * sits in, `format` asserts, and an array under `items` is the tuple form.
11
+ * A 2019-09 or 2020-12 document that relies on any of those meaning what the
12
+ * NEWER draft says will be enforced as less, or as something else, than it
13
+ * states.
14
+ */
15
+ readonly assumeDraft07?: boolean | undefined;
16
+ }
17
+ /**
18
+ * The released dialects Luq recognises the NAME of and does not implement,
19
+ * each with what it does that Draft-07 does not. They are written out so that
20
+ * "this is a dialect we know and did not build" and "we have never heard of
21
+ * this URI" stay different answers — the same distinction NON_DRAFT07_KEYWORDS
22
+ * draws between an out-of-dialect keyword and an invented one.
23
+ */
24
+ export declare const NON_DRAFT07_DIALECTS: Readonly<Record<string, string>>;
25
+ /**
26
+ * Reads the root `$schema` and refuses a dialect Luq does not implement.
27
+ *
28
+ * Called at both front doors, immediately after "is this a schema at all" and
29
+ * before any keyword is read, so no validator that enforces less than its
30
+ * document states can be reached by either route.
31
+ *
32
+ * A non-string `$schema` is a malformed VALUE rather than an unimplemented
33
+ * dialect — the meta-schema types it as a string — so it goes to the class that
34
+ * answers that question, and it is refused under the opt-out too: the opt-out
35
+ * says "read this dialect as Draft-07", and a number names no dialect.
36
+ */
37
+ export declare function assertSupportedDialect(document: unknown, options: DialectOptions): void;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NON_DRAFT07_DIALECTS = void 0;
4
+ exports.assertSupportedDialect = assertSupportedDialect;
5
+ // ===========================================================================
6
+ // L8 src/json-schema/assert-supported-dialect.ts — the DIALECT gate, read
7
+ // once at the document root before any keyword is.
8
+ //
9
+ // WHY A DEFAULT REFUSAL AND NOT A DIVERGENCE DETECTOR. Refusing only documents
10
+ // that actually USE something 2019-09 changed sounds more precise and is the
11
+ // worse trade. The divergences are not all spellable as keyword names: `$ref`
12
+ // gains siblings, `format` and the two `content*` keywords stop asserting by
13
+ // default, and 2020-12 moves the tuple form of `items` to `prefixItems`. A
14
+ // detector would therefore have to carry a second semantic model of two
15
+ // dialects this library does not implement, and every case it fails to think
16
+ // of is a SILENT wrong answer — which is the exact failure this file exists to
17
+ // stop. The `$ref`-sibling case was itself invisible until it was hunted for.
18
+ // `$schema` is the document's own statement of what it means, so that is what
19
+ // is read.
20
+ //
21
+ // WHAT THE DEFAULT COSTS, and why the opt-out exists. A newer `$schema` over a
22
+ // body that happens to be Draft-07-compatible builds correctly today, and
23
+ // people copy the newest `$schema` line into documents that use nothing new.
24
+ // Refusing outright would break those. `assumeDraft07` gives them a one-word
25
+ // fix that is written at the CALL SITE, in the caller's own source, where a
26
+ // reviewer reads it — so the reading is a stated decision rather than a silent
27
+ // default. There is no way to get the wrong answer without having written the
28
+ // word down.
29
+ //
30
+ // A document with NO `$schema` is untouched. That is most real documents and
31
+ // all 929 cases of the conformance corpus, and it is not a guess about the
32
+ // dialect: nothing was declared, so nothing contradicts Draft-07.
33
+ //
34
+ // THE ROOT ONLY. `externalDocuments` is a map the caller loads, and the suite's
35
+ // own map holds 37 documents across four dialects of which a given case follows
36
+ // two or three; refusing at registration would refuse documents nothing reads.
37
+ // A `$schema` deeper in a document is likewise not the document's dialect.
38
+ // ===========================================================================
39
+ const types_1 = require("../types");
40
+ const malformed_schema_error_1 = require("./malformed-schema-error");
41
+ const unsupported_dialect_error_1 = require("./unsupported-dialect-error");
42
+ /**
43
+ * Comparable form of a meta-schema URI: case folded, scheme dropped, and a
44
+ * trailing "#" or "/" removed.
45
+ *
46
+ * The scheme is dropped because the two families disagree about it — Draft-07
47
+ * publishes `http://json-schema.org/draft-07/schema#` and 2020-12 publishes
48
+ * `https://json-schema.org/draft/2020-12/schema` — and a document that writes
49
+ * `https` for the first or `http` for the second is naming the same dialect,
50
+ * not a different one. Matching the published string exactly would make the
51
+ * gate depend on which of two equally common spellings a document picked.
52
+ */
53
+ function normaliseDialectUri(uri) {
54
+ return uri
55
+ .trim()
56
+ .toLowerCase()
57
+ .replace(/^https?:\/\//, "")
58
+ .replace(/[#/]+$/, "");
59
+ }
60
+ const DRAFT07_NORMALISED = normaliseDialectUri("http://json-schema.org/draft-07/schema#");
61
+ /**
62
+ * The released dialects Luq recognises the NAME of and does not implement,
63
+ * each with what it does that Draft-07 does not. They are written out so that
64
+ * "this is a dialect we know and did not build" and "we have never heard of
65
+ * this URI" stay different answers — the same distinction NON_DRAFT07_KEYWORDS
66
+ * draws between an out-of-dialect keyword and an invented one.
67
+ */
68
+ exports.NON_DRAFT07_DIALECTS = Object.freeze({
69
+ "json-schema.org/schema": "the unversioned alias, which names whichever draft the site publishes " +
70
+ "as current rather than a fixed one",
71
+ "json-schema.org/draft-03/schema": "Draft-03, which spells `required` as a boolean on each property",
72
+ "json-schema.org/draft-04/schema": "Draft-04, whose `exclusiveMaximum` is a boolean modifier, not a bound",
73
+ "json-schema.org/draft-06/schema": "Draft-06, which has no `$comment` and reads `$ref` the Draft-07 way",
74
+ "json-schema.org/draft/2019-09/schema": "2019-09, where `$ref` is an ordinary applicator and its siblings are " +
75
+ "applied, and `format` asserts nothing unless the vocabulary asks",
76
+ "json-schema.org/draft/2020-12/schema": "2020-12, where `$ref` siblings are applied, the tuple form of `items` " +
77
+ "is spelled `prefixItems`, and `format` asserts nothing by default",
78
+ });
79
+ const UNRECOGNISED_DIALECT = "a meta-schema Luq has no reading for, so what its keywords mean is unknown";
80
+ const DIALECT_VALUE_REASON = "the value must be a URI string";
81
+ /**
82
+ * Reads the root `$schema` and refuses a dialect Luq does not implement.
83
+ *
84
+ * Called at both front doors, immediately after "is this a schema at all" and
85
+ * before any keyword is read, so no validator that enforces less than its
86
+ * document states can be reached by either route.
87
+ *
88
+ * A non-string `$schema` is a malformed VALUE rather than an unimplemented
89
+ * dialect — the meta-schema types it as a string — so it goes to the class that
90
+ * answers that question, and it is refused under the opt-out too: the opt-out
91
+ * says "read this dialect as Draft-07", and a number names no dialect.
92
+ */
93
+ function assertSupportedDialect(document, options) {
94
+ // Read off an untyped object rather than through `Draft07SchemaObject`,
95
+ // which types `$schema` as a string: the document is whatever JSON.parse
96
+ // produced, and a `$schema` that is not a string is exactly what this has to
97
+ // be able to see. The §4.4 boolean form and every non-object fall out here,
98
+ // declaring no dialect.
99
+ if (!(0, types_1.isPlainObject)(document))
100
+ return;
101
+ const declared = document["$schema"];
102
+ if (declared === undefined)
103
+ return;
104
+ if (typeof declared !== "string") {
105
+ throw new malformed_schema_error_1.MalformedSchemaError("$schema", DIALECT_VALUE_REASON, declared);
106
+ }
107
+ const normalised = normaliseDialectUri(declared);
108
+ if (normalised === DRAFT07_NORMALISED)
109
+ return;
110
+ if (options.assumeDraft07 === true)
111
+ return;
112
+ throw new unsupported_dialect_error_1.UnsupportedDialectError(declared, exports.NON_DRAFT07_DIALECTS[normalised] ?? UNRECOGNISED_DIALECT);
113
+ }
@@ -0,0 +1,109 @@
1
+ // ===========================================================================
2
+ // L8 src/json-schema/assert-supported-dialect.ts — the DIALECT gate, read
3
+ // once at the document root before any keyword is.
4
+ //
5
+ // WHY A DEFAULT REFUSAL AND NOT A DIVERGENCE DETECTOR. Refusing only documents
6
+ // that actually USE something 2019-09 changed sounds more precise and is the
7
+ // worse trade. The divergences are not all spellable as keyword names: `$ref`
8
+ // gains siblings, `format` and the two `content*` keywords stop asserting by
9
+ // default, and 2020-12 moves the tuple form of `items` to `prefixItems`. A
10
+ // detector would therefore have to carry a second semantic model of two
11
+ // dialects this library does not implement, and every case it fails to think
12
+ // of is a SILENT wrong answer — which is the exact failure this file exists to
13
+ // stop. The `$ref`-sibling case was itself invisible until it was hunted for.
14
+ // `$schema` is the document's own statement of what it means, so that is what
15
+ // is read.
16
+ //
17
+ // WHAT THE DEFAULT COSTS, and why the opt-out exists. A newer `$schema` over a
18
+ // body that happens to be Draft-07-compatible builds correctly today, and
19
+ // people copy the newest `$schema` line into documents that use nothing new.
20
+ // Refusing outright would break those. `assumeDraft07` gives them a one-word
21
+ // fix that is written at the CALL SITE, in the caller's own source, where a
22
+ // reviewer reads it — so the reading is a stated decision rather than a silent
23
+ // default. There is no way to get the wrong answer without having written the
24
+ // word down.
25
+ //
26
+ // A document with NO `$schema` is untouched. That is most real documents and
27
+ // all 929 cases of the conformance corpus, and it is not a guess about the
28
+ // dialect: nothing was declared, so nothing contradicts Draft-07.
29
+ //
30
+ // THE ROOT ONLY. `externalDocuments` is a map the caller loads, and the suite's
31
+ // own map holds 37 documents across four dialects of which a given case follows
32
+ // two or three; refusing at registration would refuse documents nothing reads.
33
+ // A `$schema` deeper in a document is likewise not the document's dialect.
34
+ // ===========================================================================
35
+ import { isPlainObject } from "../types/index.mjs";
36
+ import { MalformedSchemaError } from "./malformed-schema-error.mjs";
37
+ import { UnsupportedDialectError } from "./unsupported-dialect-error.mjs";
38
+ /**
39
+ * Comparable form of a meta-schema URI: case folded, scheme dropped, and a
40
+ * trailing "#" or "/" removed.
41
+ *
42
+ * The scheme is dropped because the two families disagree about it — Draft-07
43
+ * publishes `http://json-schema.org/draft-07/schema#` and 2020-12 publishes
44
+ * `https://json-schema.org/draft/2020-12/schema` — and a document that writes
45
+ * `https` for the first or `http` for the second is naming the same dialect,
46
+ * not a different one. Matching the published string exactly would make the
47
+ * gate depend on which of two equally common spellings a document picked.
48
+ */
49
+ function normaliseDialectUri(uri) {
50
+ return uri
51
+ .trim()
52
+ .toLowerCase()
53
+ .replace(/^https?:\/\//, "")
54
+ .replace(/[#/]+$/, "");
55
+ }
56
+ const DRAFT07_NORMALISED = normaliseDialectUri("http://json-schema.org/draft-07/schema#");
57
+ /**
58
+ * The released dialects Luq recognises the NAME of and does not implement,
59
+ * each with what it does that Draft-07 does not. They are written out so that
60
+ * "this is a dialect we know and did not build" and "we have never heard of
61
+ * this URI" stay different answers — the same distinction NON_DRAFT07_KEYWORDS
62
+ * draws between an out-of-dialect keyword and an invented one.
63
+ */
64
+ export const NON_DRAFT07_DIALECTS = Object.freeze({
65
+ "json-schema.org/schema": "the unversioned alias, which names whichever draft the site publishes " +
66
+ "as current rather than a fixed one",
67
+ "json-schema.org/draft-03/schema": "Draft-03, which spells `required` as a boolean on each property",
68
+ "json-schema.org/draft-04/schema": "Draft-04, whose `exclusiveMaximum` is a boolean modifier, not a bound",
69
+ "json-schema.org/draft-06/schema": "Draft-06, which has no `$comment` and reads `$ref` the Draft-07 way",
70
+ "json-schema.org/draft/2019-09/schema": "2019-09, where `$ref` is an ordinary applicator and its siblings are " +
71
+ "applied, and `format` asserts nothing unless the vocabulary asks",
72
+ "json-schema.org/draft/2020-12/schema": "2020-12, where `$ref` siblings are applied, the tuple form of `items` " +
73
+ "is spelled `prefixItems`, and `format` asserts nothing by default",
74
+ });
75
+ const UNRECOGNISED_DIALECT = "a meta-schema Luq has no reading for, so what its keywords mean is unknown";
76
+ const DIALECT_VALUE_REASON = "the value must be a URI string";
77
+ /**
78
+ * Reads the root `$schema` and refuses a dialect Luq does not implement.
79
+ *
80
+ * Called at both front doors, immediately after "is this a schema at all" and
81
+ * before any keyword is read, so no validator that enforces less than its
82
+ * document states can be reached by either route.
83
+ *
84
+ * A non-string `$schema` is a malformed VALUE rather than an unimplemented
85
+ * dialect — the meta-schema types it as a string — so it goes to the class that
86
+ * answers that question, and it is refused under the opt-out too: the opt-out
87
+ * says "read this dialect as Draft-07", and a number names no dialect.
88
+ */
89
+ export function assertSupportedDialect(document, options) {
90
+ // Read off an untyped object rather than through `Draft07SchemaObject`,
91
+ // which types `$schema` as a string: the document is whatever JSON.parse
92
+ // produced, and a `$schema` that is not a string is exactly what this has to
93
+ // be able to see. The §4.4 boolean form and every non-object fall out here,
94
+ // declaring no dialect.
95
+ if (!isPlainObject(document))
96
+ return;
97
+ const declared = document["$schema"];
98
+ if (declared === undefined)
99
+ return;
100
+ if (typeof declared !== "string") {
101
+ throw new MalformedSchemaError("$schema", DIALECT_VALUE_REASON, declared);
102
+ }
103
+ const normalised = normaliseDialectUri(declared);
104
+ if (normalised === DRAFT07_NORMALISED)
105
+ return;
106
+ if (options.assumeDraft07 === true)
107
+ return;
108
+ throw new UnsupportedDialectError(declared, NON_DRAFT07_DIALECTS[normalised] ?? UNRECOGNISED_DIALECT);
109
+ }
@@ -2,6 +2,7 @@ import type { FieldBuilderSurface } from "../builder/builder-surface.types";
2
2
  import type { FieldEntry } from "../builder/field-entry.types";
3
3
  import type { Validator } from "../builder/validator.types";
4
4
  import type { GlobalConfig } from "../types/global-config";
5
+ import type { DialectOptions } from "./assert-supported-dialect";
5
6
  import type { JsonSchemaBag } from "./json-schema-bag.types";
6
7
  /** A value that is not a schema at all. Named, and refused at the boundary. */
7
8
  export declare class NotASchemaError extends Error {
@@ -9,10 +10,28 @@ export declare class NotASchemaError extends Error {
9
10
  constructor(received: unknown);
10
11
  }
11
12
  /** One pending `.v()` per declared path, with its callback still unrun. */
12
- export declare function buildFieldEntries(bag: JsonSchemaBag, schema: unknown): readonly FieldEntry[];
13
+ export declare function buildFieldEntries(bag: JsonSchemaBag, schema: unknown, options?: DialectOptions): readonly FieldEntry[];
13
14
  /** The erased front door: the declarations, with nothing compiled yet. */
14
- export declare function buildFromSchema(bag: JsonSchemaBag, schema: unknown, config?: GlobalConfig): FieldBuilderSurface;
15
+ export declare function buildFromSchema(bag: JsonSchemaBag, schema: unknown, config?: GlobalConfig, options?: DialectOptions): FieldBuilderSurface;
15
16
  /**
17
+ * Converts a JSON Schema document into a validator.
18
+ *
19
+ * THE INPUT LIMIT IS DRAFT-07, and it is a limit on what may be handed in, not
20
+ * only a statement of how much of Draft-07 is covered. A document declaring
21
+ * 2019-09 or 2020-12 in its root `$schema` is REFUSED with an
22
+ * `UnsupportedDialectError` rather than read as Draft-07, because the two
23
+ * dialects disagree about what an unchanged keyword means: from 2019-09 on
24
+ * `$ref` is an ordinary applicator whose siblings are applied, while Draft-07
25
+ * §8.3 replaces the node, so `{"$ref": "#/$defs/name", "minLength": 5}` read as
26
+ * Draft-07 loses the `minLength` and accepts a value the document forbids. A
27
+ * document with NO `$schema` is read as Draft-07 and always has been. Pass
28
+ * `{ assumeDraft07: true }` as the fourth argument to read a newer-dialect
29
+ * document under Draft-07 rules deliberately, with that reading's consequences.
30
+ *
31
+ * `T` defaults to `Record<string, unknown>`, never `any`; under the default NO
32
+ * declared path is checked. See the header of this file, which owns that
33
+ * escape hatch.
34
+ *
16
35
  * The declared type is put back on by `eraseSchemaValidator`, the fourth
17
36
  * function in src/core/type-erasure.ts — the one file the code standard allows
18
37
  * to assert. Step 25 expressed this as an OVERLOAD PAIR instead, because
@@ -20,4 +39,4 @@ export declare function buildFromSchema(bag: JsonSchemaBag, schema: unknown, con
20
39
  * TypeScript on lenient compatibility rules and so was never audited anywhere.
21
40
  * The call below is, in the file where every escape hatch is reviewed together.
22
41
  */
23
- export declare function fromJsonSchema<T extends object = Record<string, unknown>>(bag: JsonSchemaBag, schema: unknown, config?: GlobalConfig): Validator<T>;
42
+ export declare function fromJsonSchema<T extends object = Record<string, unknown>>(bag: JsonSchemaBag, schema: unknown, config?: GlobalConfig, options?: DialectOptions): Validator<T>;
@@ -6,6 +6,7 @@ exports.buildFromSchema = buildFromSchema;
6
6
  exports.fromJsonSchema = fromJsonSchema;
7
7
  const create_field_builder_1 = require("../builder/create-field-builder");
8
8
  const type_erasure_1 = require("../core/type-erasure");
9
+ const assert_supported_dialect_1 = require("./assert-supported-dialect");
9
10
  const create_structural_context_1 = require("./create-structural-context");
10
11
  const declare_presence_1 = require("./declare-presence");
11
12
  const ref_scope_1 = require("./ref-scope");
@@ -35,9 +36,12 @@ function collectDeclaredRules(declaration, bag, root, chain) {
35
36
  ]);
36
37
  }
37
38
  /** One pending `.v()` per declared path, with its callback still unrun. */
38
- function buildFieldEntries(bag, schema) {
39
+ function buildFieldEntries(bag, schema, options = {}) {
39
40
  if (!(0, draft07_types_1.isDraft07Schema)(schema))
40
41
  throw new NotASchemaError(schema);
42
+ // Before any keyword is read: what the document's keywords MEAN depends on
43
+ // the dialect it declares, and Luq reads every one of them as Draft-07.
44
+ (0, assert_supported_dialect_1.assertSupportedDialect)(schema, options);
41
45
  return (0, flatten_schema_1.flattenSchema)(schema, schema_to_declarations_1.readChildSchemas).map((declaration) => ({
42
46
  path: declaration.path,
43
47
  defaultOf: null,
@@ -53,10 +57,28 @@ function buildFieldEntries(bag, schema) {
53
57
  }));
54
58
  }
55
59
  /** The erased front door: the declarations, with nothing compiled yet. */
56
- function buildFromSchema(bag, schema, config) {
57
- return (0, create_field_builder_1.createFieldBuilderSurface)(bag, config, buildFieldEntries(bag, schema));
60
+ function buildFromSchema(bag, schema, config, options) {
61
+ return (0, create_field_builder_1.createFieldBuilderSurface)(bag, config, buildFieldEntries(bag, schema, options));
58
62
  }
59
63
  /**
64
+ * Converts a JSON Schema document into a validator.
65
+ *
66
+ * THE INPUT LIMIT IS DRAFT-07, and it is a limit on what may be handed in, not
67
+ * only a statement of how much of Draft-07 is covered. A document declaring
68
+ * 2019-09 or 2020-12 in its root `$schema` is REFUSED with an
69
+ * `UnsupportedDialectError` rather than read as Draft-07, because the two
70
+ * dialects disagree about what an unchanged keyword means: from 2019-09 on
71
+ * `$ref` is an ordinary applicator whose siblings are applied, while Draft-07
72
+ * §8.3 replaces the node, so `{"$ref": "#/$defs/name", "minLength": 5}` read as
73
+ * Draft-07 loses the `minLength` and accepts a value the document forbids. A
74
+ * document with NO `$schema` is read as Draft-07 and always has been. Pass
75
+ * `{ assumeDraft07: true }` as the fourth argument to read a newer-dialect
76
+ * document under Draft-07 rules deliberately, with that reading's consequences.
77
+ *
78
+ * `T` defaults to `Record<string, unknown>`, never `any`; under the default NO
79
+ * declared path is checked. See the header of this file, which owns that
80
+ * escape hatch.
81
+ *
60
82
  * The declared type is put back on by `eraseSchemaValidator`, the fourth
61
83
  * function in src/core/type-erasure.ts — the one file the code standard allows
62
84
  * to assert. Step 25 expressed this as an OVERLOAD PAIR instead, because
@@ -64,7 +86,7 @@ function buildFromSchema(bag, schema, config) {
64
86
  * TypeScript on lenient compatibility rules and so was never audited anywhere.
65
87
  * The call below is, in the file where every escape hatch is reviewed together.
66
88
  */
67
- function fromJsonSchema(bag, schema, config) {
68
- const planBacked = buildFromSchema(bag, schema, config).build();
89
+ function fromJsonSchema(bag, schema, config, options) {
90
+ const planBacked = buildFromSchema(bag, schema, config, options).build();
69
91
  return (0, type_erasure_1.eraseSchemaValidator)(planBacked);
70
92
  }
@@ -1,5 +1,6 @@
1
1
  import { createFieldBuilderSurface } from "../builder/create-field-builder.mjs";
2
2
  import { eraseSchemaValidator } from "../core/type-erasure.mjs";
3
+ import { assertSupportedDialect } from "./assert-supported-dialect.mjs";
3
4
  import { createStructuralContext } from "./create-structural-context.mjs";
4
5
  import { declarePresenceRules } from "./declare-presence.mjs";
5
6
  import { createLocalScope } from "./ref-scope.mjs";
@@ -28,9 +29,12 @@ function collectDeclaredRules(declaration, bag, root, chain) {
28
29
  ]);
29
30
  }
30
31
  /** One pending `.v()` per declared path, with its callback still unrun. */
31
- export function buildFieldEntries(bag, schema) {
32
+ export function buildFieldEntries(bag, schema, options = {}) {
32
33
  if (!isDraft07Schema(schema))
33
34
  throw new NotASchemaError(schema);
35
+ // Before any keyword is read: what the document's keywords MEAN depends on
36
+ // the dialect it declares, and Luq reads every one of them as Draft-07.
37
+ assertSupportedDialect(schema, options);
34
38
  return flattenSchema(schema, readChildSchemas).map((declaration) => ({
35
39
  path: declaration.path,
36
40
  defaultOf: null,
@@ -46,10 +50,28 @@ export function buildFieldEntries(bag, schema) {
46
50
  }));
47
51
  }
48
52
  /** The erased front door: the declarations, with nothing compiled yet. */
49
- export function buildFromSchema(bag, schema, config) {
50
- return createFieldBuilderSurface(bag, config, buildFieldEntries(bag, schema));
53
+ export function buildFromSchema(bag, schema, config, options) {
54
+ return createFieldBuilderSurface(bag, config, buildFieldEntries(bag, schema, options));
51
55
  }
52
56
  /**
57
+ * Converts a JSON Schema document into a validator.
58
+ *
59
+ * THE INPUT LIMIT IS DRAFT-07, and it is a limit on what may be handed in, not
60
+ * only a statement of how much of Draft-07 is covered. A document declaring
61
+ * 2019-09 or 2020-12 in its root `$schema` is REFUSED with an
62
+ * `UnsupportedDialectError` rather than read as Draft-07, because the two
63
+ * dialects disagree about what an unchanged keyword means: from 2019-09 on
64
+ * `$ref` is an ordinary applicator whose siblings are applied, while Draft-07
65
+ * §8.3 replaces the node, so `{"$ref": "#/$defs/name", "minLength": 5}` read as
66
+ * Draft-07 loses the `minLength` and accepts a value the document forbids. A
67
+ * document with NO `$schema` is read as Draft-07 and always has been. Pass
68
+ * `{ assumeDraft07: true }` as the fourth argument to read a newer-dialect
69
+ * document under Draft-07 rules deliberately, with that reading's consequences.
70
+ *
71
+ * `T` defaults to `Record<string, unknown>`, never `any`; under the default NO
72
+ * declared path is checked. See the header of this file, which owns that
73
+ * escape hatch.
74
+ *
53
75
  * The declared type is put back on by `eraseSchemaValidator`, the fourth
54
76
  * function in src/core/type-erasure.ts — the one file the code standard allows
55
77
  * to assert. Step 25 expressed this as an OVERLOAD PAIR instead, because
@@ -57,7 +79,7 @@ export function buildFromSchema(bag, schema, config) {
57
79
  * TypeScript on lenient compatibility rules and so was never audited anywhere.
58
80
  * The call below is, in the file where every escape hatch is reviewed together.
59
81
  */
60
- export function fromJsonSchema(bag, schema, config) {
61
- const planBacked = buildFromSchema(bag, schema, config).build();
82
+ export function fromJsonSchema(bag, schema, config, options) {
83
+ const planBacked = buildFromSchema(bag, schema, config, options).build();
62
84
  return eraseSchemaValidator(planBacked);
63
85
  }
@@ -1,4 +1,4 @@
1
1
  export { SCHEMA_BRANCH_LABEL, collectDocumentRules, jsonSchemaPlugin, } from "./json-schema";
2
2
  export type { JsonSchemaOptions } from "./json-schema";
3
- export { MalformedSchemaError, NotASchemaError, UnsupportedKeywordError, RefResolutionError, buildFieldEntries, buildFromSchema, fromJsonSchema, isDraft07Schema, listBoundPluginNames, listDraft07Keywords, listFormatNames, } from "../../index";
4
- export type { Draft07Schema, Draft07SchemaObject, JsonSchemaBag, } from "../../index";
3
+ export { DRAFT07_DIALECT_URI, MalformedSchemaError, NotASchemaError, UnsupportedDialectError, UnsupportedKeywordError, RefResolutionError, buildFieldEntries, buildFromSchema, fromJsonSchema, isDraft07Schema, listBoundPluginNames, listDraft07Keywords, listFormatNames, } from "../../index";
4
+ export type { DialectOptions, Draft07Schema, Draft07SchemaObject, JsonSchemaBag, } from "../../index";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.listFormatNames = exports.listDraft07Keywords = exports.listBoundPluginNames = exports.isDraft07Schema = exports.fromJsonSchema = exports.buildFromSchema = exports.buildFieldEntries = exports.RefResolutionError = exports.UnsupportedKeywordError = exports.NotASchemaError = exports.MalformedSchemaError = exports.jsonSchemaPlugin = exports.collectDocumentRules = exports.SCHEMA_BRANCH_LABEL = void 0;
3
+ exports.listFormatNames = exports.listDraft07Keywords = exports.listBoundPluginNames = exports.isDraft07Schema = exports.fromJsonSchema = exports.buildFromSchema = exports.buildFieldEntries = exports.RefResolutionError = exports.UnsupportedKeywordError = exports.UnsupportedDialectError = exports.NotASchemaError = exports.MalformedSchemaError = exports.DRAFT07_DIALECT_URI = exports.jsonSchemaPlugin = exports.collectDocumentRules = exports.SCHEMA_BRANCH_LABEL = void 0;
4
4
  // ===========================================================================
5
5
  // The public subpath `@maroonedog/luq/plugins/jsonSchema`. Re-exports only.
6
6
  //
@@ -15,8 +15,10 @@ Object.defineProperty(exports, "SCHEMA_BRANCH_LABEL", { enumerable: true, get: f
15
15
  Object.defineProperty(exports, "collectDocumentRules", { enumerable: true, get: function () { return json_schema_1.collectDocumentRules; } });
16
16
  Object.defineProperty(exports, "jsonSchemaPlugin", { enumerable: true, get: function () { return json_schema_1.jsonSchemaPlugin; } });
17
17
  var index_1 = require("../../index");
18
+ Object.defineProperty(exports, "DRAFT07_DIALECT_URI", { enumerable: true, get: function () { return index_1.DRAFT07_DIALECT_URI; } });
18
19
  Object.defineProperty(exports, "MalformedSchemaError", { enumerable: true, get: function () { return index_1.MalformedSchemaError; } });
19
20
  Object.defineProperty(exports, "NotASchemaError", { enumerable: true, get: function () { return index_1.NotASchemaError; } });
21
+ Object.defineProperty(exports, "UnsupportedDialectError", { enumerable: true, get: function () { return index_1.UnsupportedDialectError; } });
20
22
  Object.defineProperty(exports, "UnsupportedKeywordError", { enumerable: true, get: function () { return index_1.UnsupportedKeywordError; } });
21
23
  Object.defineProperty(exports, "RefResolutionError", { enumerable: true, get: function () { return index_1.RefResolutionError; } });
22
24
  Object.defineProperty(exports, "buildFieldEntries", { enumerable: true, get: function () { return index_1.buildFieldEntries; } });
@@ -8,4 +8,4 @@
8
8
  // a consumer of this subpath pays only for the plugins they import themselves.
9
9
  // ===========================================================================
10
10
  export { SCHEMA_BRANCH_LABEL, collectDocumentRules, jsonSchemaPlugin, } from "./json-schema.mjs";
11
- export { MalformedSchemaError, NotASchemaError, UnsupportedKeywordError, RefResolutionError, buildFieldEntries, buildFromSchema, fromJsonSchema, isDraft07Schema, listBoundPluginNames, listDraft07Keywords, listFormatNames, } from "../../index.mjs";
11
+ export { DRAFT07_DIALECT_URI, MalformedSchemaError, NotASchemaError, UnsupportedDialectError, UnsupportedKeywordError, RefResolutionError, buildFieldEntries, buildFromSchema, fromJsonSchema, isDraft07Schema, listBoundPluginNames, listDraft07Keywords, listFormatNames, } from "../../index.mjs";
@@ -2,7 +2,7 @@ import type { Rule } from "../../../plugin-kit/compiled-rule";
2
2
  import { type MessageContextExtra, type TypeName } from "../../../types";
3
3
  import type { RuleBuildContext } from "../../../plugin-kit/rule-build-context";
4
4
  import type { Unchanged } from "../../../plugin-kit/marker.types";
5
- import { type JsonSchemaBag } from "../../index";
5
+ import { type DialectOptions, type JsonSchemaBag } from "../../index";
6
6
  /** The label the whole document carries inside the composite it becomes. */
7
7
  export declare const SCHEMA_BRANCH_LABEL = "schema";
8
8
  /**
@@ -17,7 +17,7 @@ export declare const SCHEMA_BRANCH_LABEL = "schema";
17
17
  * The caller fetches, reads from disk, or bundles — whichever is right for
18
18
  * their deployment — and hands over what they already have.
19
19
  */
20
- export interface JsonSchemaOptions {
20
+ export interface JsonSchemaOptions extends DialectOptions {
21
21
  readonly externalDocuments?: Readonly<Record<string, unknown>> | undefined;
22
22
  }
23
23
  /**
@@ -66,6 +66,9 @@ const NO_EXTERNAL_DOCUMENTS = Object.freeze({});
66
66
  function collectDocumentRules(ctx, document, bag, options = {}) {
67
67
  if (!(0, index_1.isDraft07Schema)(document))
68
68
  throw new index_1.NotASchemaError(document);
69
+ // Before any keyword is read, and in the same place the function front door
70
+ // reads it: the dialect decides what the keywords below MEAN.
71
+ (0, index_1.assertSupportedDialect)(document, options);
69
72
  const root = document;
70
73
  const scope = (0, index_1.createDocumentScope)(root, options.externalDocuments ?? NO_EXTERNAL_DOCUMENTS);
71
74
  // The seed's `chain` is a ChainBuildContext, whose three members a
@@ -38,7 +38,7 @@
38
38
  import { branch, composite } from "../../../plugin-kit/create-rule.mjs";
39
39
  import { definePlugin } from "../../../plugin-kit/plugin-definition.mjs";
40
40
  import { PASS } from "../../../types/index.mjs";
41
- import { NotASchemaError, collectSubSchemaRules, createStructuralContext, createDocumentScope, isDraft07Schema, resolveSchemaNodeInScope, } from "../../index.mjs";
41
+ import { NotASchemaError, assertSupportedDialect, collectSubSchemaRules, createStructuralContext, createDocumentScope, isDraft07Schema, resolveSchemaNodeInScope, } from "../../index.mjs";
42
42
  /** Every slot: a JSON Schema document constrains a value of any shape. */
43
43
  const SCHEMA_SLOTS = [
44
44
  "string",
@@ -62,6 +62,9 @@ const NO_EXTERNAL_DOCUMENTS = Object.freeze({});
62
62
  export function collectDocumentRules(ctx, document, bag, options = {}) {
63
63
  if (!isDraft07Schema(document))
64
64
  throw new NotASchemaError(document);
65
+ // Before any keyword is read, and in the same place the function front door
66
+ // reads it: the dialect decides what the keywords below MEAN.
67
+ assertSupportedDialect(document, options);
65
68
  const root = document;
66
69
  const scope = createDocumentScope(root, options.externalDocuments ?? NO_EXTERNAL_DOCUMENTS);
67
70
  // The seed's `chain` is a ChainBuildContext, whose three members a
@@ -2,4 +2,6 @@ export { fromJsonSchema, jsonSchemaFullFeaturePlugin, } from "./json-schema-full
2
2
  export { jsonSchemaBag } from "./bundled-plugins";
3
3
  export { UNBOUND_BUNDLED_PLUGIN_NAMES, findStaleUnboundDeclarations, findUnbundledBoundPlugins, findUnexplainedBundledPlugins, listBundledPluginNames, } from "./bundle-coverage";
4
4
  export type { JsonSchemaOptions } from "../json-schema";
5
- export type { JsonSchemaBag } from "../../index";
5
+ export { DRAFT07_DIALECT_URI, UnsupportedDialectError, } from "../../unsupported-dialect-error";
6
+ export { MalformedSchemaError, NotASchemaError, RefResolutionError, UnsupportedKeywordError, } from "../../index";
7
+ export type { DialectOptions, JsonSchemaBag } from "../../index";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.listBundledPluginNames = exports.findUnexplainedBundledPlugins = exports.findUnbundledBoundPlugins = exports.findStaleUnboundDeclarations = exports.UNBOUND_BUNDLED_PLUGIN_NAMES = exports.jsonSchemaBag = exports.jsonSchemaFullFeaturePlugin = exports.fromJsonSchema = void 0;
3
+ exports.UnsupportedKeywordError = exports.RefResolutionError = exports.NotASchemaError = exports.MalformedSchemaError = exports.UnsupportedDialectError = exports.DRAFT07_DIALECT_URI = exports.listBundledPluginNames = exports.findUnexplainedBundledPlugins = exports.findUnbundledBoundPlugins = exports.findStaleUnboundDeclarations = exports.UNBOUND_BUNDLED_PLUGIN_NAMES = exports.jsonSchemaBag = exports.jsonSchemaFullFeaturePlugin = exports.fromJsonSchema = void 0;
4
4
  // ===========================================================================
5
5
  // The public subpath `@maroonedog/luq/plugins/jsonSchemaFullFeature`.
6
6
  // Re-exports only.
@@ -23,3 +23,20 @@ Object.defineProperty(exports, "findStaleUnboundDeclarations", { enumerable: tru
23
23
  Object.defineProperty(exports, "findUnbundledBoundPlugins", { enumerable: true, get: function () { return bundle_coverage_1.findUnbundledBoundPlugins; } });
24
24
  Object.defineProperty(exports, "findUnexplainedBundledPlugins", { enumerable: true, get: function () { return bundle_coverage_1.findUnexplainedBundledPlugins; } });
25
25
  Object.defineProperty(exports, "listBundledPluginNames", { enumerable: true, get: function () { return bundle_coverage_1.listBundledPluginNames; } });
26
+ // Every error `fromJsonSchema` can throw, so a caller can tell them apart by
27
+ // class rather than by reading the message.
28
+ //
29
+ // This subpath is the one-import route and the one the documentation points at,
30
+ // which made it the worst place to be missing them: a document that is not
31
+ // Draft-07 at all, one the meta-schema forbids, one carrying a keyword Luq
32
+ // cannot honour and one that is not a schema are four different things to do
33
+ // about it, and a caller who can only match on message text has to re-derive
34
+ // that distinction from prose that is free to change.
35
+ var unsupported_dialect_error_1 = require("../../unsupported-dialect-error");
36
+ Object.defineProperty(exports, "DRAFT07_DIALECT_URI", { enumerable: true, get: function () { return unsupported_dialect_error_1.DRAFT07_DIALECT_URI; } });
37
+ Object.defineProperty(exports, "UnsupportedDialectError", { enumerable: true, get: function () { return unsupported_dialect_error_1.UnsupportedDialectError; } });
38
+ var index_1 = require("../../index");
39
+ Object.defineProperty(exports, "MalformedSchemaError", { enumerable: true, get: function () { return index_1.MalformedSchemaError; } });
40
+ Object.defineProperty(exports, "NotASchemaError", { enumerable: true, get: function () { return index_1.NotASchemaError; } });
41
+ Object.defineProperty(exports, "RefResolutionError", { enumerable: true, get: function () { return index_1.RefResolutionError; } });
42
+ Object.defineProperty(exports, "UnsupportedKeywordError", { enumerable: true, get: function () { return index_1.UnsupportedKeywordError; } });
@@ -12,3 +12,14 @@
12
12
  export { fromJsonSchema, jsonSchemaFullFeaturePlugin, } from "./json-schema-full-feature.mjs";
13
13
  export { jsonSchemaBag } from "./bundled-plugins.mjs";
14
14
  export { UNBOUND_BUNDLED_PLUGIN_NAMES, findStaleUnboundDeclarations, findUnbundledBoundPlugins, findUnexplainedBundledPlugins, listBundledPluginNames, } from "./bundle-coverage.mjs";
15
+ // Every error `fromJsonSchema` can throw, so a caller can tell them apart by
16
+ // class rather than by reading the message.
17
+ //
18
+ // This subpath is the one-import route and the one the documentation points at,
19
+ // which made it the worst place to be missing them: a document that is not
20
+ // Draft-07 at all, one the meta-schema forbids, one carrying a keyword Luq
21
+ // cannot honour and one that is not a schema are four different things to do
22
+ // about it, and a caller who can only match on message text has to re-derive
23
+ // that distinction from prose that is free to change.
24
+ export { DRAFT07_DIALECT_URI, UnsupportedDialectError, } from "../../unsupported-dialect-error.mjs";
25
+ export { MalformedSchemaError, NotASchemaError, RefResolutionError, UnsupportedKeywordError, } from "../../index.mjs";
@@ -1,6 +1,7 @@
1
1
  import type { MessageContextExtra, TypeName } from "../../../types";
2
2
  import type { Unchanged } from "../../../plugin-kit/marker.types";
3
3
  import type { JsonSchemaOptions } from "../json-schema";
4
+ import type { DialectOptions } from "../../index";
4
5
  import type { GlobalConfig } from "../../../types/global-config";
5
6
  export declare const jsonSchemaFullFeaturePlugin: import("../../../plugin-kit/plugin-definition").PluginDefinition<"jsonSchemaFullFeature", "jsonSchemaFullFeature", readonly TypeName[], {
6
7
  args: readonly [document: unknown, options?: JsonSchemaOptions];
@@ -17,8 +18,15 @@ export declare const jsonSchemaFullFeaturePlugin: import("../../../plugin-kit/pl
17
18
  * NO declared path is checked — see build-from-schema.ts, which owns that
18
19
  * escape hatch and its single overload.
19
20
  *
21
+ * THE INPUT LIMIT IS DRAFT-07. A document whose root `$schema` names 2019-09 or
22
+ * 2020-12 is refused with an `UnsupportedDialectError` rather than read as
23
+ * Draft-07, because the dialects disagree about what unchanged keywords mean;
24
+ * a document with no `$schema` is read as Draft-07 as before. Pass
25
+ * `{ assumeDraft07: true }` as the third argument to take the Draft-07 reading
26
+ * deliberately. build-from-schema.ts states the divergence in full.
27
+ *
20
28
  * The return type is INFERRED rather than written: naming `Validator<T>` would
21
29
  * mean importing src/builder, which tier `extension` forbids. The inferred type
22
30
  * is that same `Validator<T>` and the type test asserts it.
23
31
  */
24
- export declare function fromJsonSchema<T extends object = Record<string, unknown>>(schema: unknown, config?: GlobalConfig): import("../../..").Validator<T, T>;
32
+ export declare function fromJsonSchema<T extends object = Record<string, unknown>>(schema: unknown, config?: GlobalConfig, options?: DialectOptions): import("../../..").Validator<T, T>;
@@ -57,10 +57,17 @@ exports.jsonSchemaFullFeaturePlugin = (0, plugin_definition_1.definePlugin)()({
57
57
  * NO declared path is checked — see build-from-schema.ts, which owns that
58
58
  * escape hatch and its single overload.
59
59
  *
60
+ * THE INPUT LIMIT IS DRAFT-07. A document whose root `$schema` names 2019-09 or
61
+ * 2020-12 is refused with an `UnsupportedDialectError` rather than read as
62
+ * Draft-07, because the dialects disagree about what unchanged keywords mean;
63
+ * a document with no `$schema` is read as Draft-07 as before. Pass
64
+ * `{ assumeDraft07: true }` as the third argument to take the Draft-07 reading
65
+ * deliberately. build-from-schema.ts states the divergence in full.
66
+ *
60
67
  * The return type is INFERRED rather than written: naming `Validator<T>` would
61
68
  * mean importing src/builder, which tier `extension` forbids. The inferred type
62
69
  * is that same `Validator<T>` and the type test asserts it.
63
70
  */
64
- function fromJsonSchema(schema, config) {
65
- return (0, index_1.fromJsonSchema)(bundled_plugins_1.jsonSchemaBag, schema, config);
71
+ function fromJsonSchema(schema, config, options) {
72
+ return (0, index_1.fromJsonSchema)(bundled_plugins_1.jsonSchemaBag, schema, config, options);
66
73
  }
@@ -53,10 +53,17 @@ export const jsonSchemaFullFeaturePlugin = /*#__PURE__*/ definePlugin()({
53
53
  * NO declared path is checked — see build-from-schema.ts, which owns that
54
54
  * escape hatch and its single overload.
55
55
  *
56
+ * THE INPUT LIMIT IS DRAFT-07. A document whose root `$schema` names 2019-09 or
57
+ * 2020-12 is refused with an `UnsupportedDialectError` rather than read as
58
+ * Draft-07, because the dialects disagree about what unchanged keywords mean;
59
+ * a document with no `$schema` is read as Draft-07 as before. Pass
60
+ * `{ assumeDraft07: true }` as the third argument to take the Draft-07 reading
61
+ * deliberately. build-from-schema.ts states the divergence in full.
62
+ *
56
63
  * The return type is INFERRED rather than written: naming `Validator<T>` would
57
64
  * mean importing src/builder, which tier `extension` forbids. The inferred type
58
65
  * is that same `Validator<T>` and the type test asserts it.
59
66
  */
60
- export function fromJsonSchema(schema, config) {
61
- return convertWithBag(jsonSchemaBag, schema, config);
67
+ export function fromJsonSchema(schema, config, options) {
68
+ return convertWithBag(jsonSchemaBag, schema, config, options);
62
69
  }
@@ -18,6 +18,9 @@ export type { SchemaRegistry } from "./schema-registry";
18
18
  export { createSchemaRegistry } from "./schema-registry";
19
19
  export { UnsupportedKeywordError } from "./unsupported-keyword-error";
20
20
  export { MalformedSchemaError } from "./malformed-schema-error";
21
+ export { DRAFT07_DIALECT_URI, UnsupportedDialectError, } from "./unsupported-dialect-error";
22
+ export { NON_DRAFT07_DIALECTS, assertSupportedDialect, } from "./assert-supported-dialect";
23
+ export type { DialectOptions } from "./assert-supported-dialect";
21
24
  export { isDraft07Schema, isSchemaObject } from "./draft07.types";
22
25
  export type { Draft07Schema, Draft07SchemaObject, Draft07TypeKeyword, } from "./draft07.types";
23
26
  export { assertKeywordSupported, countKeywordHandlings, findKeywordHandling, isDraft07Keyword, listBoundPluginNames, listDraft07Keywords, } from "./keyword-map";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.listFormatNames = exports.findFormatHandling = exports.listDraft07Keywords = exports.listBoundPluginNames = exports.isDraft07Keyword = exports.findKeywordHandling = exports.countKeywordHandlings = exports.assertKeywordSupported = exports.isSchemaObject = exports.isDraft07Schema = exports.MalformedSchemaError = exports.UnsupportedKeywordError = exports.createSchemaRegistry = exports.createLocalScope = exports.createDocumentScope = exports.resolveRefInScope = exports.resolveRef = exports.isResolvableRef = exports.RefResolutionError = exports.declarePresenceRules = exports.declareRequiredProperties = exports.createStructuralContext = exports.toSchemaBranch = exports.collectSubSchemaRules = exports.readChildSchemas = exports.isStructuralKeyword = exports.expandSchemaRules = exports.STRUCTURAL_EXPANSIONS = exports.joinDeclaredPath = exports.flattenSchema = exports.toSchemaObject = exports.resolveSchemaNodeInScope = exports.resolveSchemaNode = exports.readRefPointer = exports.isDefinitionContainer = exports.collectDefinitionNames = exports.DEFINITION_CONTAINERS = exports.fromJsonSchema = exports.buildFromSchema = exports.buildFieldEntries = exports.NotASchemaError = void 0;
3
+ exports.listFormatNames = exports.findFormatHandling = exports.listDraft07Keywords = exports.listBoundPluginNames = exports.isDraft07Keyword = exports.findKeywordHandling = exports.countKeywordHandlings = exports.assertKeywordSupported = exports.isSchemaObject = exports.isDraft07Schema = exports.assertSupportedDialect = exports.NON_DRAFT07_DIALECTS = exports.UnsupportedDialectError = exports.DRAFT07_DIALECT_URI = exports.MalformedSchemaError = exports.UnsupportedKeywordError = exports.createSchemaRegistry = exports.createLocalScope = exports.createDocumentScope = exports.resolveRefInScope = exports.resolveRef = exports.isResolvableRef = exports.RefResolutionError = exports.declarePresenceRules = exports.declareRequiredProperties = exports.createStructuralContext = exports.toSchemaBranch = exports.collectSubSchemaRules = exports.readChildSchemas = exports.isStructuralKeyword = exports.expandSchemaRules = exports.STRUCTURAL_EXPANSIONS = exports.joinDeclaredPath = exports.flattenSchema = exports.toSchemaObject = exports.resolveSchemaNodeInScope = exports.resolveSchemaNode = exports.readRefPointer = exports.isDefinitionContainer = exports.collectDefinitionNames = exports.DEFINITION_CONTAINERS = exports.fromJsonSchema = exports.buildFromSchema = exports.buildFieldEntries = exports.NotASchemaError = void 0;
4
4
  // ===========================================================================
5
5
  // L8 src/json-schema/index.ts — re-exports only. Nothing is defined here.
6
6
  //
@@ -52,6 +52,12 @@ var unsupported_keyword_error_1 = require("./unsupported-keyword-error");
52
52
  Object.defineProperty(exports, "UnsupportedKeywordError", { enumerable: true, get: function () { return unsupported_keyword_error_1.UnsupportedKeywordError; } });
53
53
  var malformed_schema_error_1 = require("./malformed-schema-error");
54
54
  Object.defineProperty(exports, "MalformedSchemaError", { enumerable: true, get: function () { return malformed_schema_error_1.MalformedSchemaError; } });
55
+ var unsupported_dialect_error_1 = require("./unsupported-dialect-error");
56
+ Object.defineProperty(exports, "DRAFT07_DIALECT_URI", { enumerable: true, get: function () { return unsupported_dialect_error_1.DRAFT07_DIALECT_URI; } });
57
+ Object.defineProperty(exports, "UnsupportedDialectError", { enumerable: true, get: function () { return unsupported_dialect_error_1.UnsupportedDialectError; } });
58
+ var assert_supported_dialect_1 = require("./assert-supported-dialect");
59
+ Object.defineProperty(exports, "NON_DRAFT07_DIALECTS", { enumerable: true, get: function () { return assert_supported_dialect_1.NON_DRAFT07_DIALECTS; } });
60
+ Object.defineProperty(exports, "assertSupportedDialect", { enumerable: true, get: function () { return assert_supported_dialect_1.assertSupportedDialect; } });
55
61
  var draft07_types_1 = require("./draft07.types");
56
62
  Object.defineProperty(exports, "isDraft07Schema", { enumerable: true, get: function () { return draft07_types_1.isDraft07Schema; } });
57
63
  Object.defineProperty(exports, "isSchemaObject", { enumerable: true, get: function () { return draft07_types_1.isSchemaObject; } });
@@ -18,6 +18,8 @@ export { createDocumentScope, createLocalScope } from "./ref-scope.mjs";
18
18
  export { createSchemaRegistry } from "./schema-registry.mjs";
19
19
  export { UnsupportedKeywordError } from "./unsupported-keyword-error.mjs";
20
20
  export { MalformedSchemaError } from "./malformed-schema-error.mjs";
21
+ export { DRAFT07_DIALECT_URI, UnsupportedDialectError, } from "./unsupported-dialect-error.mjs";
22
+ export { NON_DRAFT07_DIALECTS, assertSupportedDialect, } from "./assert-supported-dialect.mjs";
21
23
  export { isDraft07Schema, isSchemaObject } from "./draft07.types.mjs";
22
24
  export { assertKeywordSupported, countKeywordHandlings, findKeywordHandling, isDraft07Keyword, listBoundPluginNames, listDraft07Keywords, } from "./keyword-map.mjs";
23
25
  export { findFormatHandling, listFormatNames } from "./format-map.mjs";
@@ -25,7 +25,9 @@ const ANNOTATION_ONLY = "an annotation with no validation effect in Draft-07";
25
25
  */
26
26
  exports.constBinding = (0, bind_keyword_1.bindKeyword)("any", "literal", literal_1.literalPlugin, (v) => [v]);
27
27
  exports.coreKeywordMap = {
28
- $schema: (0, bind_keyword_1.unsupported)("read and discarded: Luq targets Draft-07 and does not switch dialect"),
28
+ $schema: (0, bind_keyword_1.unsupported)("read at the document ROOT by assert-supported-dialect and then " +
29
+ "discarded: Luq implements Draft-07 and never switches dialect, so a " +
30
+ "root `$schema` naming another one is refused rather than ignored"),
29
31
  $id: (0, bind_keyword_1.unsupported)("no base-URI resolution; resolve-ref accepts local $ref only"),
30
32
  $comment: (0, bind_keyword_1.unsupported)(ANNOTATION_ONLY),
31
33
  $ref: (0, bind_keyword_1.structural)("resolved by resolve-ref before any keyword is read"),
@@ -22,7 +22,9 @@ const ANNOTATION_ONLY = "an annotation with no validation effect in Draft-07";
22
22
  */
23
23
  export const constBinding = bindKeyword("any", "literal", literalPlugin, (v) => [v]);
24
24
  export const coreKeywordMap = {
25
- $schema: unsupported("read and discarded: Luq targets Draft-07 and does not switch dialect"),
25
+ $schema: unsupported("read at the document ROOT by assert-supported-dialect and then " +
26
+ "discarded: Luq implements Draft-07 and never switches dialect, so a " +
27
+ "root `$schema` naming another one is refused rather than ignored"),
26
28
  $id: unsupported("no base-URI resolution; resolve-ref accepts local $ref only"),
27
29
  $comment: unsupported(ANNOTATION_ONLY),
28
30
  $ref: structural("resolved by resolve-ref before any keyword is read"),
@@ -32,7 +32,10 @@ exports.NON_DRAFT07_KEYWORDS = {
32
32
  $defs: "2019-09 spelling of `definitions`; resolve-ref follows it anyway",
33
33
  $anchor: "2019-09 anchors; resolve-ref resolves JSON Pointers only",
34
34
  $dynamicRef: "2020-12 dynamic references",
35
+ $dynamicAnchor: "2020-12 dynamic anchors; the target half of $dynamicRef",
35
36
  $recursiveRef: "2019-09 recursive references",
37
+ $recursiveAnchor: "2019-09 recursive anchors; the target half of $recursiveRef",
38
+ $vocabulary: "2019-09 vocabulary declaration; Luq has one fixed vocabulary",
36
39
  dependentRequired: "2019-09 split of `dependencies`; use `dependencies`",
37
40
  dependentSchemas: "2019-09 split of `dependencies`; use `dependencies`",
38
41
  minContains: "2019-09 bound on `contains`; Draft-07's is fixed at one",
@@ -23,7 +23,10 @@ export const NON_DRAFT07_KEYWORDS = {
23
23
  $defs: "2019-09 spelling of `definitions`; resolve-ref follows it anyway",
24
24
  $anchor: "2019-09 anchors; resolve-ref resolves JSON Pointers only",
25
25
  $dynamicRef: "2020-12 dynamic references",
26
+ $dynamicAnchor: "2020-12 dynamic anchors; the target half of $dynamicRef",
26
27
  $recursiveRef: "2019-09 recursive references",
28
+ $recursiveAnchor: "2019-09 recursive anchors; the target half of $recursiveRef",
29
+ $vocabulary: "2019-09 vocabulary declaration; Luq has one fixed vocabulary",
27
30
  dependentRequired: "2019-09 split of `dependencies`; use `dependencies`",
28
31
  dependentSchemas: "2019-09 split of `dependencies`; use `dependencies`",
29
32
  minContains: "2019-09 bound on `contains`; Draft-07's is fixed at one",
@@ -0,0 +1,21 @@
1
+ /** The dialect this library implements, in the spelling the draft publishes. */
2
+ export declare const DRAFT07_DIALECT_URI = "http://json-schema.org/draft-07/schema#";
3
+ /**
4
+ * Raised when a document's root `$schema` names a dialect Luq does not
5
+ * implement. Thrown at build time, before a validator that would read the
6
+ * document under the wrong dialect's rules can exist.
7
+ *
8
+ * `reason` follows the convention the other two refusals use: a sentence
9
+ * fragment with no terminal punctuation, saying what the dialect does that
10
+ * Draft-07 does not, so that two refusals from two different dialects read as
11
+ * one library rather than as two unrelated messages.
12
+ */
13
+ export declare class UnsupportedDialectError extends Error {
14
+ /** The `$schema` value the document wrote, verbatim. */
15
+ readonly declared: string;
16
+ /** The dialect Luq reads every document under. */
17
+ readonly implemented: string;
18
+ /** What that dialect does differently, or that it is unrecognised. */
19
+ readonly reason: string;
20
+ constructor(declared: string, reason: string);
21
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ // ===========================================================================
3
+ // L8 src/json-schema/unsupported-dialect-error.ts
4
+ //
5
+ // The third refusal, and the only one with no keyword to hang on.
6
+ // UnsupportedKeywordError refuses a keyword NAME; MalformedSchemaError refuses
7
+ // a keyword VALUE. Both need the document to write something recognisable, and
8
+ // there is one divergence that writes nothing at all:
9
+ //
10
+ // {"$schema": "https://json-schema.org/draft/2020-12/schema",
11
+ // "$defs": {"name": {"type": "string"}},
12
+ // "properties": {"nick": {"$ref": "#/$defs/name", "minLength": 5}}}
13
+ //
14
+ // Every keyword there is a Draft-07 keyword spelled the Draft-07 way, so the
15
+ // keyword table sees nothing to refuse. The meaning is not Draft-07: §8.3 has
16
+ // `$ref` REPLACE the object it appears in, and from 2019-09 on `$ref` is an
17
+ // ordinary applicator whose siblings are applied. Read as Draft-07 the document
18
+ // above builds a validator that accepts `{"nick":"ab"}` — the `minLength` is
19
+ // gone, silently, and nothing in the result says a constraint went missing.
20
+ // `$schema` is the only signal the document offers, so it is the one read.
21
+ //
22
+ // It stays a separate class from the other two, neither extending the other,
23
+ // for the reason written in malformed-schema-error.ts: each answers a different
24
+ // question. "Luq cannot honour this keyword", "this document is not valid
25
+ // Draft-07", and now "this document is not Draft-07 AT ALL" are three distinct
26
+ // facts, and a caller catching one must not silently catch another.
27
+ // ===========================================================================
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.UnsupportedDialectError = exports.DRAFT07_DIALECT_URI = void 0;
30
+ /** The dialect this library implements, in the spelling the draft publishes. */
31
+ exports.DRAFT07_DIALECT_URI = "http://json-schema.org/draft-07/schema#";
32
+ /**
33
+ * Raised when a document's root `$schema` names a dialect Luq does not
34
+ * implement. Thrown at build time, before a validator that would read the
35
+ * document under the wrong dialect's rules can exist.
36
+ *
37
+ * `reason` follows the convention the other two refusals use: a sentence
38
+ * fragment with no terminal punctuation, saying what the dialect does that
39
+ * Draft-07 does not, so that two refusals from two different dialects read as
40
+ * one library rather than as two unrelated messages.
41
+ */
42
+ class UnsupportedDialectError extends Error {
43
+ constructor(declared, reason) {
44
+ super(`JSON Schema dialect "${declared}" is not supported: ${reason}. Luq ` +
45
+ `implements Draft-07 (${exports.DRAFT07_DIALECT_URI}) and would read this ` +
46
+ "document under Draft-07 rules, which can enforce less than it " +
47
+ "states. Convert the document to Draft-07, or pass " +
48
+ "`{ assumeDraft07: true }` to take the Draft-07 reading deliberately.");
49
+ /** The dialect Luq reads every document under. */
50
+ this.implemented = exports.DRAFT07_DIALECT_URI;
51
+ this.name = "UnsupportedDialectError";
52
+ this.declared = declared;
53
+ this.reason = reason;
54
+ // Without this, `instanceof` fails when the package is compiled to ES5.
55
+ Object.setPrototypeOf(this, UnsupportedDialectError.prototype);
56
+ }
57
+ }
58
+ exports.UnsupportedDialectError = UnsupportedDialectError;
@@ -0,0 +1,54 @@
1
+ // ===========================================================================
2
+ // L8 src/json-schema/unsupported-dialect-error.ts
3
+ //
4
+ // The third refusal, and the only one with no keyword to hang on.
5
+ // UnsupportedKeywordError refuses a keyword NAME; MalformedSchemaError refuses
6
+ // a keyword VALUE. Both need the document to write something recognisable, and
7
+ // there is one divergence that writes nothing at all:
8
+ //
9
+ // {"$schema": "https://json-schema.org/draft/2020-12/schema",
10
+ // "$defs": {"name": {"type": "string"}},
11
+ // "properties": {"nick": {"$ref": "#/$defs/name", "minLength": 5}}}
12
+ //
13
+ // Every keyword there is a Draft-07 keyword spelled the Draft-07 way, so the
14
+ // keyword table sees nothing to refuse. The meaning is not Draft-07: §8.3 has
15
+ // `$ref` REPLACE the object it appears in, and from 2019-09 on `$ref` is an
16
+ // ordinary applicator whose siblings are applied. Read as Draft-07 the document
17
+ // above builds a validator that accepts `{"nick":"ab"}` — the `minLength` is
18
+ // gone, silently, and nothing in the result says a constraint went missing.
19
+ // `$schema` is the only signal the document offers, so it is the one read.
20
+ //
21
+ // It stays a separate class from the other two, neither extending the other,
22
+ // for the reason written in malformed-schema-error.ts: each answers a different
23
+ // question. "Luq cannot honour this keyword", "this document is not valid
24
+ // Draft-07", and now "this document is not Draft-07 AT ALL" are three distinct
25
+ // facts, and a caller catching one must not silently catch another.
26
+ // ===========================================================================
27
+ /** The dialect this library implements, in the spelling the draft publishes. */
28
+ export const DRAFT07_DIALECT_URI = "http://json-schema.org/draft-07/schema#";
29
+ /**
30
+ * Raised when a document's root `$schema` names a dialect Luq does not
31
+ * implement. Thrown at build time, before a validator that would read the
32
+ * document under the wrong dialect's rules can exist.
33
+ *
34
+ * `reason` follows the convention the other two refusals use: a sentence
35
+ * fragment with no terminal punctuation, saying what the dialect does that
36
+ * Draft-07 does not, so that two refusals from two different dialects read as
37
+ * one library rather than as two unrelated messages.
38
+ */
39
+ export class UnsupportedDialectError extends Error {
40
+ constructor(declared, reason) {
41
+ super(`JSON Schema dialect "${declared}" is not supported: ${reason}. Luq ` +
42
+ `implements Draft-07 (${DRAFT07_DIALECT_URI}) and would read this ` +
43
+ "document under Draft-07 rules, which can enforce less than it " +
44
+ "states. Convert the document to Draft-07, or pass " +
45
+ "`{ assumeDraft07: true }` to take the Draft-07 reading deliberately.");
46
+ /** The dialect Luq reads every document under. */
47
+ this.implemented = DRAFT07_DIALECT_URI;
48
+ this.name = "UnsupportedDialectError";
49
+ this.declared = declared;
50
+ this.reason = reason;
51
+ // Without this, `instanceof` fails when the package is compiled to ES5.
52
+ Object.setPrototypeOf(this, UnsupportedDialectError.prototype);
53
+ }
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maroonedog/luq",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Universal Model & API Definition Platform - TypeScript validation library evolving into cross-language code generation",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -459,18 +459,20 @@
459
459
  "typecheck:src": "tsc --noEmit -p tsconfig.json",
460
460
  "typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
461
461
  "typecheck:bench": "tsc --noEmit -p bench/tsconfig.json",
462
- "generate": "npm run generate:sources && npm run generate:exports && npm run generate:lock",
462
+ "generate": "npm run generate:sources && npm run generate:exports && npm run generate:lock && npm run generate:issue-codes",
463
463
  "generate:sources": "npm run generate:manifest && npm run generate:barrel && npm run generate:slot-catalog",
464
464
  "generate:slot-catalog": "npx ts-node --project scripts/tsconfig.json scripts/generate-slot-catalog.ts",
465
465
  "generate:manifest": "npx ts-node --project scripts/tsconfig.json scripts/generate-plugin-manifest.ts",
466
466
  "generate:barrel": "npx ts-node --project scripts/tsconfig.json scripts/generate-plugin-barrel.ts",
467
467
  "generate:exports": "npx ts-node --project scripts/tsconfig.json scripts/generate-package-exports.ts",
468
468
  "generate:lock": "npx ts-node --project scripts/tsconfig.json scripts/generate-plugin-catalog.ts",
469
+ "generate:issue-codes": "npx ts-node --project scripts/tsconfig.json scripts/generate-issue-code-catalog.ts",
469
470
  "check:exports": "npx ts-node --project scripts/tsconfig.json scripts/check-exports.ts --mode=exact",
470
471
  "check:catalog-lock": "npx ts-node --project scripts/tsconfig.json scripts/check-catalog-lock.ts",
472
+ "check:issue-code-lock": "npx ts-node --project scripts/tsconfig.json scripts/check-issue-code-lock.ts",
471
473
  "check:plugin-isolation": "npx ts-node --project scripts/tsconfig.json scripts/check-plugin-isolation.ts",
472
474
  "check:plugin-uniqueness": "npx ts-node --project scripts/tsconfig.json scripts/check-plugin-uniqueness.ts",
473
- "check:catalog": "npm run check:plugin-uniqueness && npm run check:plugin-isolation && npm run check:exports && npm run check:catalog-lock",
475
+ "check:catalog": "npm run check:plugin-uniqueness && npm run check:plugin-isolation && npm run check:exports && npm run check:catalog-lock && npm run check:issue-code-lock",
474
476
  "check:size": "npx ts-node --project scripts/tsconfig.json scripts/measure-bundle-size.ts",
475
477
  "check:barrel-equivalence": "npx ts-node --project scripts/tsconfig.json scripts/check-barrel-equivalence.ts",
476
478
  "check:no-dynamic-code": "npx ts-node --project scripts/tsconfig.json scripts/check-no-dynamic-code.ts",
@@ -493,6 +495,7 @@
493
495
  "check:conformance-figures": "npx ts-node --project scripts/tsconfig.json scripts/check-conformance-figures.ts",
494
496
  "bench:competitors": "npx ts-node --project bench/tsconfig.json bench/competitors/report-competitors.ts",
495
497
  "bench:competitors:check": "npx ts-node --project bench/tsconfig.json bench/competitors/check-competitors.ts",
498
+ "bench:megamorphism": "npx ts-node --project bench/tsconfig.json bench/megamorphism/report-megamorphism.ts",
496
499
  "lint:ox": "oxlint -c .oxlintrc.json src bench",
497
500
  "lint:naming": "eslint \"src/**/*.ts\" \"bench/**/*.ts\"",
498
501
  "generate:perf-figures": "npx ts-node --project scripts/tsconfig.json scripts/check-perf-figures.ts --write",