@maroonedog/luq 2.4.3 → 2.5.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 (60) hide show
  1. package/README.md +199 -9
  2. package/dist/chain/field-chain.types.d.ts +11 -1
  3. package/dist/chain/index.d.ts +2 -0
  4. package/dist/chain/plugin-not-imported.types.d.ts +40 -0
  5. package/dist/chain/plugin-not-imported.types.js +2 -0
  6. package/dist/chain/plugin-not-imported.types.mjs +1 -0
  7. package/dist/chain/slot-catalog.generated.d.ts +265 -0
  8. package/dist/chain/slot-catalog.generated.js +4 -0
  9. package/dist/chain/slot-catalog.generated.mjs +3 -0
  10. package/dist/core/type-erasure.d.ts +20 -0
  11. package/dist/core/type-erasure.js +23 -0
  12. package/dist/core/type-erasure.mjs +22 -0
  13. package/dist/field-rule/use-field.d.ts +2 -1
  14. package/dist/field-rule/use-field.js +9 -2
  15. package/dist/field-rule/use-field.mjs +9 -2
  16. package/dist/json-schema/array-keyword-guards.d.ts +31 -0
  17. package/dist/json-schema/array-keyword-guards.js +120 -0
  18. package/dist/json-schema/array-keyword-guards.mjs +114 -0
  19. package/dist/json-schema/assert-object-keyword-values.d.ts +59 -0
  20. package/dist/json-schema/assert-object-keyword-values.js +149 -0
  21. package/dist/json-schema/assert-object-keyword-values.mjs +141 -0
  22. package/dist/json-schema/collect-definitions.d.ts +7 -0
  23. package/dist/json-schema/collect-definitions.js +9 -0
  24. package/dist/json-schema/collect-definitions.mjs +10 -1
  25. package/dist/json-schema/declare-additional-properties.d.ts +11 -1
  26. package/dist/json-schema/declare-additional-properties.js +13 -1
  27. package/dist/json-schema/declare-additional-properties.mjs +13 -1
  28. package/dist/json-schema/declare-object-keywords.d.ts +4 -0
  29. package/dist/json-schema/declare-object-keywords.js +11 -4
  30. package/dist/json-schema/declare-object-keywords.mjs +11 -4
  31. package/dist/json-schema/declare-required-properties.js +6 -0
  32. package/dist/json-schema/declare-required-properties.mjs +6 -0
  33. package/dist/json-schema/declare-value-keywords.d.ts +10 -1
  34. package/dist/json-schema/declare-value-keywords.js +43 -4
  35. package/dist/json-schema/declare-value-keywords.mjs +43 -4
  36. package/dist/json-schema/extensions/json-schema/index.d.ts +1 -1
  37. package/dist/json-schema/extensions/json-schema/index.js +2 -1
  38. package/dist/json-schema/extensions/json-schema/index.mjs +1 -1
  39. package/dist/json-schema/flatten-array-schema.d.ts +11 -1
  40. package/dist/json-schema/flatten-array-schema.js +20 -7
  41. package/dist/json-schema/flatten-array-schema.mjs +20 -7
  42. package/dist/json-schema/index.d.ts +1 -0
  43. package/dist/json-schema/index.js +3 -1
  44. package/dist/json-schema/index.mjs +1 -0
  45. package/dist/json-schema/keyword-map-string.js +30 -1
  46. package/dist/json-schema/keyword-map-string.mjs +30 -1
  47. package/dist/json-schema/keyword-map.js +2 -0
  48. package/dist/json-schema/keyword-map.mjs +2 -0
  49. package/dist/json-schema/malformed-schema-error.d.ts +38 -0
  50. package/dist/json-schema/malformed-schema-error.js +103 -0
  51. package/dist/json-schema/malformed-schema-error.mjs +98 -0
  52. package/dist/plugins/manifest.generated.d.ts +18 -0
  53. package/dist/plugins/manifest.generated.js +77 -77
  54. package/dist/plugins/manifest.generated.mjs +77 -77
  55. package/dist/plugins/one-of/one-of.js +18 -4
  56. package/dist/plugins/one-of/one-of.mjs +18 -4
  57. package/dist/schema-tooling/index.d.ts +11 -0
  58. package/dist/schema-tooling/index.js +12 -1
  59. package/dist/schema-tooling/index.mjs +10 -0
  60. package/package.json +8 -6
@@ -0,0 +1,141 @@
1
+ // ===========================================================================
2
+ // L8 src/json-schema/assert-object-keyword-values.ts — the meta-schema check
3
+ // on the VALUES of the object-family keywords: `required`, `properties`,
4
+ // `additionalProperties`, `patternProperties`, `propertyNames` and
5
+ // `dependencies`.
6
+ //
7
+ // They live together because they fail together: every one is read in a place
8
+ // that trusts the declared type, and untyped JSON reaches all six. `required:
9
+ // "name"` is iterated as a string and declares one phantom child per
10
+ // CHARACTER; `properties: {a: null}` is read for a pointer; the remaining four
11
+ // take the sub-schema route with a value that is not a schema at all, and the
12
+ // walker finds no keys on a primitive, so it yields a branch holding no rules.
13
+ //
14
+ // That last outcome is the SILENT one, and it is the one that matters most: a
15
+ // document saying "no extra properties", or "every key beginning with a_ holds
16
+ // a string", compiles to a validator that accepts everything. It is what a
17
+ // config service serialising booleans as strings produces, and what a
18
+ // hand-edited document with a quoted sub-schema produces. The loud failures
19
+ // are loud in the wrong place — a raw TypeError read off null somewhere deep
20
+ // in the walker, naming neither the keyword nor the document.
21
+ //
22
+ // Refusal is therefore always at BUILD time, and every well-formed shape is
23
+ // untouched: a name list, a map of schemas (including the §4.4 boolean form),
24
+ // a single schema, and — under `dependencies` — either a schema or an array of
25
+ // property names, which §6.5.7 makes equally lawful under the same key.
26
+ // ===========================================================================
27
+ import { isPlainObject, isStringArray } from "../types/index.mjs";
28
+ import { isDraft07Schema } from "./draft07.types.mjs";
29
+ import { MalformedSchemaError, SCHEMA_FORMS } from "./malformed-schema-error.mjs";
30
+ /**
31
+ * `required` is an array of property names (§6.5.3), and whatever the document
32
+ * put there is refused when it is anything else.
33
+ *
34
+ * UNIQUENESS IS NOT ENFORCED, although the meta-schema's `stringArray` asks
35
+ * for it. A repeated name asks for nothing the single name does not — the
36
+ * property is required either way, so the verdict is identical — and this
37
+ * check exists to stop a validator enforcing LESS than its document, not to
38
+ * lint a document that is enforced exactly as written. Refusing a duplicate
39
+ * would reject working schemas for no gain in correctness.
40
+ */
41
+ export function assertRequiredIsNameList(value) {
42
+ if (value === undefined || isStringArray(value))
43
+ return;
44
+ throw new MalformedSchemaError("required", "the value must be an array of property names", value);
45
+ }
46
+ /**
47
+ * `properties` and `patternProperties` differ only in what their KEYS mean, so
48
+ * they are checked the same way: the map itself, then every member of it, and
49
+ * the offending key named in the reason so the message points at one entry
50
+ * rather than at a map that may hold a dozen. `dependencies` is not routed
51
+ * through here — §6.5.7 gives its members a second lawful form, which is a
52
+ * different member check and a different sentence.
53
+ *
54
+ * `mapReason` is the keyword's own, because "an object mapping property names
55
+ * to schemas" and "an object mapping regular expressions to schemas" are the
56
+ * only part of the requirement the two keywords do not share.
57
+ */
58
+ function assertSchemaMap(keyword, mapReason, value) {
59
+ if (value === undefined)
60
+ return;
61
+ if (!isPlainObject(value)) {
62
+ throw new MalformedSchemaError(keyword, mapReason, value);
63
+ }
64
+ for (const [key, member] of Object.entries(value)) {
65
+ if (isDraft07Schema(member))
66
+ continue;
67
+ throw new MalformedSchemaError(keyword, `the schema under "${key}" ${SCHEMA_FORMS}`, member);
68
+ }
69
+ }
70
+ /**
71
+ * `properties` maps property names to schemas (§6.5.4), and a schema is an
72
+ * object or a boolean; the map itself and every member of it are refused when
73
+ * they are not. The key is named in the reason so the message points at the
74
+ * offending node rather than at the whole document.
75
+ */
76
+ export function assertPropertiesIsSchemaMap(value) {
77
+ assertSchemaMap("properties", "the value must be an object mapping property names to schemas", value);
78
+ }
79
+ /**
80
+ * `additionalProperties` is a schema, and §4.4 makes a boolean one (§6.5.6).
81
+ * Both of those forms pass through unchanged; only a value the meta-schema
82
+ * already forbids — a string, a number, an array, null — is refused.
83
+ */
84
+ export function assertAdditionalPropertiesIsSchema(value) {
85
+ if (value === undefined || isDraft07Schema(value))
86
+ return;
87
+ throw new MalformedSchemaError("additionalProperties", `the value ${SCHEMA_FORMS}`, value);
88
+ }
89
+ /**
90
+ * `patternProperties` maps regular expressions to schemas (§6.5.5). The map
91
+ * itself and every member of it are refused when they are not schemas, and the
92
+ * pattern is named in the reason so the message points at the one offending
93
+ * entry rather than at a map that may hold a dozen.
94
+ *
95
+ * THE KEYS ARE DELIBERATELY NOT COMPILED HERE, although Draft-07 says each
96
+ * SHOULD be a valid ECMA-262 pattern. A key that will not compile is not the
97
+ * failure this module exists to stop: the plugin compiles every key while
98
+ * building its rule, so such a key already kills the build, loudly, before any
99
+ * validator exists — it is a SyntaxError rather than a typed refusal, but
100
+ * nothing is silently under-enforced. Compiling one here as well would put a
101
+ * SECOND `new RegExp` in the converter, which holds exactly one — the `pattern`
102
+ * keyword's, over the document's own source string — so that regex
103
+ * construction and its flag handling stay in a single place.
104
+ */
105
+ export function assertPatternPropertiesIsSchemaMap(value) {
106
+ assertSchemaMap("patternProperties", "the value must be an object mapping regular expressions to schemas", value);
107
+ }
108
+ /**
109
+ * `propertyNames` is ONE schema, applied to every property name (§6.5.8), and
110
+ * §4.4 makes a boolean one. An ARRAY is the shape of a schema list and is not
111
+ * itself a schema, so it is refused along with every primitive. An object
112
+ * stands whatever keywords it carries, because §4.3 has a schema ignore the
113
+ * keywords it does not recognise rather than be invalid for holding them.
114
+ */
115
+ export function assertPropertyNamesIsSchema(value) {
116
+ if (value === undefined || isDraft07Schema(value))
117
+ return;
118
+ throw new MalformedSchemaError("propertyNames", `the value ${SCHEMA_FORMS}`, value);
119
+ }
120
+ /**
121
+ * `dependencies` maps a property name to what its presence demands (§6.5.7),
122
+ * and the draft gives that two equally lawful forms under the same key: an
123
+ * ARRAY of property names, which makes those names required alongside it, or a
124
+ * SCHEMA the whole instance must then satisfy. Both are kept; only a value
125
+ * that is neither is refused, and the key is named so the message points at
126
+ * the one offending entry rather than at the whole map.
127
+ */
128
+ export function assertDependenciesIsSchemaOrNameListMap(value) {
129
+ if (value === undefined)
130
+ return;
131
+ if (!isPlainObject(value)) {
132
+ throw new MalformedSchemaError("dependencies", "the value must be an object mapping property names to schemas or to " +
133
+ "arrays of property names", value);
134
+ }
135
+ for (const [key, dependency] of Object.entries(value)) {
136
+ if (isStringArray(dependency) || isDraft07Schema(dependency))
137
+ continue;
138
+ throw new MalformedSchemaError("dependencies", `the dependency under "${key}" ${SCHEMA_FORMS}, or an array of ` +
139
+ "property names", dependency);
140
+ }
141
+ }
@@ -45,6 +45,13 @@ export declare function advanceBase(scope: RefScope, node: Draft07SchemaObject):
45
45
  * a recursive definition BEFORE it walks into it: `#/definitions/node` inside
46
46
  * its own `properties` is a legitimate schema whose declared paths are
47
47
  * infinite, and Luq declares finite paths.
48
+ *
49
+ * The node is asked what it IS, not what it is not. Untyped JSON reaches here —
50
+ * a `properties` entry is whatever the document wrote — and "not a boolean"
51
+ * counts `null` as something to read `$ref` off, which throws before the
52
+ * malformed entry can be named. The pair is the same one resolve-ref uses:
53
+ * isDraft07Schema keeps out everything that is not a schema at all, and
54
+ * isSchemaObject drops the §4.4 boolean form, which carries no pointer.
48
55
  */
49
56
  export declare function readRefPointer(schema: Draft07Schema): string | undefined;
50
57
  /** Every definition name the document declares, under either spelling. */
@@ -90,8 +90,17 @@ function advanceBase(scope, node) {
90
90
  * a recursive definition BEFORE it walks into it: `#/definitions/node` inside
91
91
  * its own `properties` is a legitimate schema whose declared paths are
92
92
  * infinite, and Luq declares finite paths.
93
+ *
94
+ * The node is asked what it IS, not what it is not. Untyped JSON reaches here —
95
+ * a `properties` entry is whatever the document wrote — and "not a boolean"
96
+ * counts `null` as something to read `$ref` off, which throws before the
97
+ * malformed entry can be named. The pair is the same one resolve-ref uses:
98
+ * isDraft07Schema keeps out everything that is not a schema at all, and
99
+ * isSchemaObject drops the §4.4 boolean form, which carries no pointer.
93
100
  */
94
101
  function readRefPointer(schema) {
102
+ if (!(0, draft07_types_1.isDraft07Schema)(schema))
103
+ return undefined;
95
104
  if (!(0, draft07_types_1.isSchemaObject)(schema))
96
105
  return undefined;
97
106
  return schema.$ref;
@@ -1,4 +1,4 @@
1
- import { isSchemaObject } from "./draft07.types.mjs";
1
+ import { isDraft07Schema, isSchemaObject } from "./draft07.types.mjs";
2
2
  import { createLocalScope } from "./ref-scope.mjs";
3
3
  import { resolveRefInScope } from "./resolve-ref.mjs";
4
4
  import { nextBaseUri } from "./uri-reference.mjs";
@@ -80,8 +80,17 @@ export function advanceBase(scope, node) {
80
80
  * a recursive definition BEFORE it walks into it: `#/definitions/node` inside
81
81
  * its own `properties` is a legitimate schema whose declared paths are
82
82
  * infinite, and Luq declares finite paths.
83
+ *
84
+ * The node is asked what it IS, not what it is not. Untyped JSON reaches here —
85
+ * a `properties` entry is whatever the document wrote — and "not a boolean"
86
+ * counts `null` as something to read `$ref` off, which throws before the
87
+ * malformed entry can be named. The pair is the same one resolve-ref uses:
88
+ * isDraft07Schema keeps out everything that is not a schema at all, and
89
+ * isSchemaObject drops the §4.4 boolean form, which carries no pointer.
83
90
  */
84
91
  export function readRefPointer(schema) {
92
+ if (!isDraft07Schema(schema))
93
+ return undefined;
85
94
  if (!isSchemaObject(schema))
86
95
  return undefined;
87
96
  return schema.$ref;
@@ -15,5 +15,15 @@ export declare function patternPropertyKeys(schema: Draft07SchemaObject): readon
15
15
  * holds the keyword table and the compile-time check that the method exists.
16
16
  */
17
17
  export declare function applyAdditionalPropertiesBoolean(chain: ConverterChain<"object">, schema: Draft07SchemaObject, allowed: boolean): ConverterChain<"object">;
18
- /** The SCHEMA form: whatever matches neither a declaration nor a pattern follows it. */
18
+ /**
19
+ * The SCHEMA form: whatever matches neither a declaration nor a pattern
20
+ * follows it.
21
+ *
22
+ * This is the ONE place a non-boolean `additionalProperties` is consumed — the
23
+ * boolean form is taken by declareObjectRules before this runs — so it is
24
+ * where the value is checked. A string or a number reaching the sub-schema
25
+ * route is not a schema, expands to no rules, and turns
26
+ * `{"additionalProperties":"false"}` into an object that accepts every extra
27
+ * property; the check refuses it instead. Both well-formed forms are untouched.
28
+ */
19
29
  export declare function declareAdditionalPropertiesSchema(schema: Draft07SchemaObject, context: StructuralContext): readonly Rule[];
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.patternPropertyKeys = patternPropertyKeys;
4
4
  exports.applyAdditionalPropertiesBoolean = applyAdditionalPropertiesBoolean;
5
5
  exports.declareAdditionalPropertiesSchema = declareAdditionalPropertiesSchema;
6
+ const assert_object_keyword_values_1 = require("./assert-object-keyword-values");
6
7
  const apply_keyword_binding_1 = require("./apply-keyword-binding");
7
8
  const keyword_map_object_1 = require("./keyword-map-object");
8
9
  const NO_RULES = Object.freeze([]);
@@ -29,9 +30,20 @@ function applyAdditionalPropertiesBoolean(chain, schema, allowed) {
29
30
  ? (0, apply_keyword_binding_1.applyKeywordBinding)(chain, keyword_map_object_1.additionalPropertiesBinding, allowed)
30
31
  : chain.additionalProperties(allowed, undefined, patterns);
31
32
  }
32
- /** The SCHEMA form: whatever matches neither a declaration nor a pattern follows it. */
33
+ /**
34
+ * The SCHEMA form: whatever matches neither a declaration nor a pattern
35
+ * follows it.
36
+ *
37
+ * This is the ONE place a non-boolean `additionalProperties` is consumed — the
38
+ * boolean form is taken by declareObjectRules before this runs — so it is
39
+ * where the value is checked. A string or a number reaching the sub-schema
40
+ * route is not a schema, expands to no rules, and turns
41
+ * `{"additionalProperties":"false"}` into an object that accepts every extra
42
+ * property; the check refuses it instead. Both well-formed forms are untouched.
43
+ */
33
44
  function declareAdditionalPropertiesSchema(schema, context) {
34
45
  const additional = schema.additionalProperties;
46
+ (0, assert_object_keyword_values_1.assertAdditionalPropertiesIsSchema)(additional);
35
47
  if (additional === undefined || typeof additional === "boolean") {
36
48
  return NO_RULES;
37
49
  }
@@ -1,3 +1,4 @@
1
+ import { assertAdditionalPropertiesIsSchema } from "./assert-object-keyword-values.mjs";
1
2
  import { applyKeywordBinding } from "./apply-keyword-binding.mjs";
2
3
  import { additionalPropertiesBinding } from "./keyword-map-object.mjs";
3
4
  const NO_RULES = Object.freeze([]);
@@ -24,9 +25,20 @@ export function applyAdditionalPropertiesBoolean(chain, schema, allowed) {
24
25
  ? applyKeywordBinding(chain, additionalPropertiesBinding, allowed)
25
26
  : chain.additionalProperties(allowed, undefined, patterns);
26
27
  }
27
- /** The SCHEMA form: whatever matches neither a declaration nor a pattern follows it. */
28
+ /**
29
+ * The SCHEMA form: whatever matches neither a declaration nor a pattern
30
+ * follows it.
31
+ *
32
+ * This is the ONE place a non-boolean `additionalProperties` is consumed — the
33
+ * boolean form is taken by declareObjectRules before this runs — so it is
34
+ * where the value is checked. A string or a number reaching the sub-schema
35
+ * route is not a schema, expands to no rules, and turns
36
+ * `{"additionalProperties":"false"}` into an object that accepts every extra
37
+ * property; the check refuses it instead. Both well-formed forms are untouched.
38
+ */
28
39
  export function declareAdditionalPropertiesSchema(schema, context) {
29
40
  const additional = schema.additionalProperties;
41
+ assertAdditionalPropertiesIsSchema(additional);
30
42
  if (additional === undefined || typeof additional === "boolean") {
31
43
  return NO_RULES;
32
44
  }
@@ -14,6 +14,10 @@ export declare function declareDependencies(schema: Draft07SchemaObject, context
14
14
  * (declareRequiredProperties). A name in `required` with no `properties` entry
15
15
  * still gets a path on the empty schema, because §6.5.3 makes the two keywords
16
16
  * independent and the root distribution needs the path to exist.
17
+ *
18
+ * Both keywords are first READ here, for the root and for every nested node
19
+ * alike, so this is where both are checked. Unchecked, a string `required` is
20
+ * iterated character by character and every character becomes a declared path.
17
21
  */
18
22
  export declare function readPropertyChildren(schema: Draft07SchemaObject): readonly {
19
23
  step: string;
@@ -22,14 +22,12 @@ exports.readPropertyChildren = readPropertyChildren;
22
22
  const create_chain_node_1 = require("../chain/create-chain-node");
23
23
  const create_field_slots_1 = require("../chain/create-field-slots");
24
24
  const types_1 = require("../types");
25
+ const assert_object_keyword_values_1 = require("./assert-object-keyword-values");
25
26
  const declare_additional_properties_1 = require("./declare-additional-properties");
26
27
  const apply_keyword_binding_1 = require("./apply-keyword-binding");
27
28
  const collect_definitions_1 = require("./collect-definitions");
28
29
  const keyword_map_object_1 = require("./keyword-map-object");
29
30
  const NO_RULES = Object.freeze([]);
30
- function readRules(chain) {
31
- return (0, create_chain_node_1.readChainRules)(chain) ?? NO_RULES;
32
- }
33
31
  function declareObjectRules(schema, context) {
34
32
  let chain = (0, create_field_slots_1.createFieldSlots)(context.bag, context.build).object;
35
33
  if (schema.minProperties !== undefined) {
@@ -46,11 +44,12 @@ function declareObjectRules(schema, context) {
46
44
  // stays: it holds the keyword table and the check that the method exists.
47
45
  chain = (0, declare_additional_properties_1.applyAdditionalPropertiesBoolean)(chain, schema, additional);
48
46
  }
49
- return readRules(chain);
47
+ return (0, create_chain_node_1.readChainRules)(chain) ?? NO_RULES;
50
48
  }
51
49
  /** EVERY matching pattern applies; 1.x broke after the first match. */
52
50
  function declarePatternProperties(schema, context) {
53
51
  const patterns = schema.patternProperties;
52
+ (0, assert_object_keyword_values_1.assertPatternPropertiesIsSchemaMap)(patterns);
54
53
  if (patterns === undefined)
55
54
  return NO_RULES;
56
55
  const byPattern = {};
@@ -65,6 +64,7 @@ function declarePatternProperties(schema, context) {
65
64
  /** A sub-chain over the property NAMES, which are always strings. */
66
65
  function declarePropertyNames(schema, context) {
67
66
  const names = schema.propertyNames;
67
+ (0, assert_object_keyword_values_1.assertPropertyNamesIsSchema)(names);
68
68
  if (names === undefined)
69
69
  return NO_RULES;
70
70
  const plugin = context.bag.objectPropertyNames;
@@ -86,6 +86,7 @@ function splitDependencies(dependencies) {
86
86
  }
87
87
  function declareDependencies(schema, context) {
88
88
  const dependencies = schema.dependencies;
89
+ (0, assert_object_keyword_values_1.assertDependenciesIsSchemaOrNameListMap)(dependencies);
89
90
  if (dependencies === undefined)
90
91
  return NO_RULES;
91
92
  const split = splitDependencies(dependencies);
@@ -115,8 +116,14 @@ function declareDependencies(schema, context) {
115
116
  * (declareRequiredProperties). A name in `required` with no `properties` entry
116
117
  * still gets a path on the empty schema, because §6.5.3 makes the two keywords
117
118
  * independent and the root distribution needs the path to exist.
119
+ *
120
+ * Both keywords are first READ here, for the root and for every nested node
121
+ * alike, so this is where both are checked. Unchecked, a string `required` is
122
+ * iterated character by character and every character becomes a declared path.
118
123
  */
119
124
  function readPropertyChildren(schema) {
125
+ (0, assert_object_keyword_values_1.assertPropertiesIsSchemaMap)(schema.properties);
126
+ (0, assert_object_keyword_values_1.assertRequiredIsNameList)(schema.required);
120
127
  const properties = schema.properties ?? {};
121
128
  const required = schema.required ?? [];
122
129
  const children = Object.entries(properties).map(([key, member]) => ({
@@ -15,14 +15,12 @@
15
15
  import { readChainRules } from "../chain/create-chain-node.mjs";
16
16
  import { createFieldSlots } from "../chain/create-field-slots.mjs";
17
17
  import { isStringArray } from "../types/index.mjs";
18
+ import { assertDependenciesIsSchemaOrNameListMap, assertPatternPropertiesIsSchemaMap, assertPropertiesIsSchemaMap, assertPropertyNamesIsSchema, assertRequiredIsNameList, } from "./assert-object-keyword-values.mjs";
18
19
  import { applyAdditionalPropertiesBoolean } from "./declare-additional-properties.mjs";
19
20
  import { applyKeywordBinding } from "./apply-keyword-binding.mjs";
20
21
  import { toSchemaObject } from "./collect-definitions.mjs";
21
22
  import { maxPropertiesBinding, minPropertiesBinding, } from "./keyword-map-object.mjs";
22
23
  const NO_RULES = Object.freeze([]);
23
- function readRules(chain) {
24
- return readChainRules(chain) ?? NO_RULES;
25
- }
26
24
  export function declareObjectRules(schema, context) {
27
25
  let chain = createFieldSlots(context.bag, context.build).object;
28
26
  if (schema.minProperties !== undefined) {
@@ -39,11 +37,12 @@ export function declareObjectRules(schema, context) {
39
37
  // stays: it holds the keyword table and the check that the method exists.
40
38
  chain = applyAdditionalPropertiesBoolean(chain, schema, additional);
41
39
  }
42
- return readRules(chain);
40
+ return readChainRules(chain) ?? NO_RULES;
43
41
  }
44
42
  /** EVERY matching pattern applies; 1.x broke after the first match. */
45
43
  export function declarePatternProperties(schema, context) {
46
44
  const patterns = schema.patternProperties;
45
+ assertPatternPropertiesIsSchemaMap(patterns);
47
46
  if (patterns === undefined)
48
47
  return NO_RULES;
49
48
  const byPattern = {};
@@ -58,6 +57,7 @@ export function declarePatternProperties(schema, context) {
58
57
  /** A sub-chain over the property NAMES, which are always strings. */
59
58
  export function declarePropertyNames(schema, context) {
60
59
  const names = schema.propertyNames;
60
+ assertPropertyNamesIsSchema(names);
61
61
  if (names === undefined)
62
62
  return NO_RULES;
63
63
  const plugin = context.bag.objectPropertyNames;
@@ -79,6 +79,7 @@ function splitDependencies(dependencies) {
79
79
  }
80
80
  export function declareDependencies(schema, context) {
81
81
  const dependencies = schema.dependencies;
82
+ assertDependenciesIsSchemaOrNameListMap(dependencies);
82
83
  if (dependencies === undefined)
83
84
  return NO_RULES;
84
85
  const split = splitDependencies(dependencies);
@@ -108,8 +109,14 @@ export function declareDependencies(schema, context) {
108
109
  * (declareRequiredProperties). A name in `required` with no `properties` entry
109
110
  * still gets a path on the empty schema, because §6.5.3 makes the two keywords
110
111
  * independent and the root distribution needs the path to exist.
112
+ *
113
+ * Both keywords are first READ here, for the root and for every nested node
114
+ * alike, so this is where both are checked. Unchecked, a string `required` is
115
+ * iterated character by character and every character becomes a declared path.
111
116
  */
112
117
  export function readPropertyChildren(schema) {
118
+ assertPropertiesIsSchemaMap(schema.properties);
119
+ assertRequiredIsNameList(schema.required);
113
120
  const properties = schema.properties ?? {};
114
121
  const required = schema.required ?? [];
115
122
  const children = Object.entries(properties).map(([key, member]) => ({
@@ -19,6 +19,7 @@ exports.declareRequiredProperties = declareRequiredProperties;
19
19
  // ===========================================================================
20
20
  const create_rule_1 = require("../plugin-kit/create-rule");
21
21
  const types_1 = require("../types");
22
+ const assert_object_keyword_values_1 = require("./assert-object-keyword-values");
22
23
  const NO_RULES = Object.freeze([]);
23
24
  /**
24
25
  * `required` as a rule on the OBJECT, not as a presence rule on each child.
@@ -36,6 +37,11 @@ const NO_RULES = Object.freeze([]);
36
37
  */
37
38
  function declareRequiredProperties(schema, context) {
38
39
  const required = schema.required;
40
+ // Checked again here, and not only where the children are read: this rule
41
+ // closes over `required` and calls `.filter` on it at VALIDATION time, so an
42
+ // unchecked non-array escapes the build and surfaces as a raw TypeError on
43
+ // the first request.
44
+ (0, assert_object_keyword_values_1.assertRequiredIsNameList)(required);
39
45
  if (required === undefined || required.length === 0)
40
46
  return NO_RULES;
41
47
  const missingOf = (value) => required.filter((key) => !Object.prototype.hasOwnProperty.call(value, key));
@@ -16,6 +16,7 @@
16
16
  // ===========================================================================
17
17
  import { check } from "../plugin-kit/create-rule.mjs";
18
18
  import { PASS, fail, isArray, isPlainObject } from "../types/index.mjs";
19
+ import { assertRequiredIsNameList } from "./assert-object-keyword-values.mjs";
19
20
  const NO_RULES = Object.freeze([]);
20
21
  /**
21
22
  * `required` as a rule on the OBJECT, not as a presence rule on each child.
@@ -33,6 +34,11 @@ const NO_RULES = Object.freeze([]);
33
34
  */
34
35
  export function declareRequiredProperties(schema, context) {
35
36
  const required = schema.required;
37
+ // Checked again here, and not only where the children are read: this rule
38
+ // closes over `required` and calls `.filter` on it at VALIDATION time, so an
39
+ // unchecked non-array escapes the build and surfaces as a raw TypeError on
40
+ // the first request.
41
+ assertRequiredIsNameList(required);
36
42
  if (required === undefined || required.length === 0)
37
43
  return NO_RULES;
38
44
  const missingOf = (value) => required.filter((key) => !Object.prototype.hasOwnProperty.call(value, key));
@@ -1,7 +1,16 @@
1
1
  import type { Rule } from "../plugin-kit/compiled-rule";
2
2
  import type { Draft07SchemaObject, Draft07TypeKeyword } from "./draft07.types";
3
3
  import type { StructuralContext } from "./structural-expansion.types";
4
- /** The declared names, always as a list, with unknown names rejected loudly. */
4
+ /**
5
+ * The declared names, always as a list. Every departure from the meta-schema
6
+ * is a refusal rather than a name dropped from the list: a dropped name leaves
7
+ * NO type rule at all (the list is what declareTypeRules counts) and also
8
+ * turns permitsNull true, so `{"type":"strig"}` would both stop checking the
9
+ * type and start accepting null.
10
+ *
11
+ * `schema.type` is typed by draft07.types.ts, but the document it came from is
12
+ * plain JSON that nothing has checked, so the value is re-read as `unknown`.
13
+ */
5
14
  export declare function readDeclaredTypes(schema: Draft07SchemaObject): readonly Draft07TypeKeyword[];
6
15
  /** True when the document permits an explicit null at this position. */
7
16
  export declare function permitsNull(schema: Draft07SchemaObject): boolean;
@@ -19,6 +19,11 @@ exports.declareEnumRules = declareEnumRules;
19
19
  // primitive type (`object` is the only type plugin in the catalogue and it is
20
20
  // not in the bag).
21
21
  //
22
+ // `type` is also the ONE keyword here whose value is refused rather than
23
+ // normalised, because it is the only rule in the converter that judges the
24
+ // value's type: a name this module cannot read would take the whole check with
25
+ // it. See readDeclaredTypes.
26
+ //
22
27
  // `null` never reaches a check — src/runtime/decide-presence.ts settles absence
23
28
  // first — so `type: "null"` reads as "every value that got this far is wrong",
24
29
  // which is exactly right: the only null-shaped value was already accepted by
@@ -30,8 +35,9 @@ const create_rule_1 = require("../plugin-kit/create-rule");
30
35
  const types_1 = require("../types");
31
36
  const apply_keyword_binding_1 = require("./apply-keyword-binding");
32
37
  const keyword_map_core_1 = require("./keyword-map-core");
38
+ const malformed_schema_error_1 = require("./malformed-schema-error");
33
39
  const NO_RULES = Object.freeze([]);
34
- /** Every Draft-07 `type` name, so an unknown one is a build-time refusal. */
40
+ /** The seven names, and the test each one stands for. Nothing else is a type. */
35
41
  const JSON_TYPE_TESTS = {
36
42
  string: types_1.isString,
37
43
  number: (value) => typeof value === "number",
@@ -44,13 +50,46 @@ const JSON_TYPE_TESTS = {
44
50
  function isTypeKeyword(name) {
45
51
  return Object.prototype.hasOwnProperty.call(JSON_TYPE_TESTS, name);
46
52
  }
47
- /** The declared names, always as a list, with unknown names rejected loudly. */
53
+ /**
54
+ * One member of `type`, checked against the seven names.
55
+ *
56
+ * The string check is not decoration and must stay FIRST: the lookup below is
57
+ * a property lookup, so a non-string key is coerced to its string form, and
58
+ * `null` coerces to the name "null" — an unguarded lookup therefore reads
59
+ * `{"type": null}` as `{"type": "null"}` and changes what the document says.
60
+ */
61
+ function readTypeName(name) {
62
+ if (!(0, types_1.isString)(name)) {
63
+ throw new malformed_schema_error_1.MalformedSchemaError("type", "every member of the array form must be a string", name);
64
+ }
65
+ if (!isTypeKeyword(name)) {
66
+ throw new malformed_schema_error_1.MalformedSchemaError("type", `the name "${name}" must be one of the seven type names`, name);
67
+ }
68
+ return name;
69
+ }
70
+ /**
71
+ * The declared names, always as a list. Every departure from the meta-schema
72
+ * is a refusal rather than a name dropped from the list: a dropped name leaves
73
+ * NO type rule at all (the list is what declareTypeRules counts) and also
74
+ * turns permitsNull true, so `{"type":"strig"}` would both stop checking the
75
+ * type and start accepting null.
76
+ *
77
+ * `schema.type` is typed by draft07.types.ts, but the document it came from is
78
+ * plain JSON that nothing has checked, so the value is re-read as `unknown`.
79
+ */
48
80
  function readDeclaredTypes(schema) {
49
81
  const declared = schema.type;
50
82
  if (declared === undefined)
51
83
  return Object.freeze([]);
52
- const names = (0, types_1.isArray)(declared) ? declared : [declared];
53
- return Object.freeze(names.filter((name) => isTypeKeyword(name)));
84
+ if ((0, types_1.isString)(declared))
85
+ return Object.freeze([readTypeName(declared)]);
86
+ if (!(0, types_1.isArray)(declared)) {
87
+ throw new malformed_schema_error_1.MalformedSchemaError("type", "the value must be a type name or an array of type names", declared);
88
+ }
89
+ if (declared.length === 0) {
90
+ throw new malformed_schema_error_1.MalformedSchemaError("type", "the array form must hold at least one name (minItems 1)", declared);
91
+ }
92
+ return Object.freeze(declared.map((name) => readTypeName(name)));
54
93
  }
55
94
  /** True when the document permits an explicit null at this position. */
56
95
  function permitsNull(schema) {
@@ -11,6 +11,11 @@
11
11
  // primitive type (`object` is the only type plugin in the catalogue and it is
12
12
  // not in the bag).
13
13
  //
14
+ // `type` is also the ONE keyword here whose value is refused rather than
15
+ // normalised, because it is the only rule in the converter that judges the
16
+ // value's type: a name this module cannot read would take the whole check with
17
+ // it. See readDeclaredTypes.
18
+ //
14
19
  // `null` never reaches a check — src/runtime/decide-presence.ts settles absence
15
20
  // first — so `type: "null"` reads as "every value that got this far is wrong",
16
21
  // which is exactly right: the only null-shaped value was already accepted by
@@ -22,8 +27,9 @@ import { check } from "../plugin-kit/create-rule.mjs";
22
27
  import { PASS, fail, isArray, isPlainObject, isString } from "../types/index.mjs";
23
28
  import { applyKeywordBinding } from "./apply-keyword-binding.mjs";
24
29
  import { constBinding } from "./keyword-map-core.mjs";
30
+ import { MalformedSchemaError } from "./malformed-schema-error.mjs";
25
31
  const NO_RULES = Object.freeze([]);
26
- /** Every Draft-07 `type` name, so an unknown one is a build-time refusal. */
32
+ /** The seven names, and the test each one stands for. Nothing else is a type. */
27
33
  const JSON_TYPE_TESTS = {
28
34
  string: isString,
29
35
  number: (value) => typeof value === "number",
@@ -36,13 +42,46 @@ const JSON_TYPE_TESTS = {
36
42
  function isTypeKeyword(name) {
37
43
  return Object.prototype.hasOwnProperty.call(JSON_TYPE_TESTS, name);
38
44
  }
39
- /** The declared names, always as a list, with unknown names rejected loudly. */
45
+ /**
46
+ * One member of `type`, checked against the seven names.
47
+ *
48
+ * The string check is not decoration and must stay FIRST: the lookup below is
49
+ * a property lookup, so a non-string key is coerced to its string form, and
50
+ * `null` coerces to the name "null" — an unguarded lookup therefore reads
51
+ * `{"type": null}` as `{"type": "null"}` and changes what the document says.
52
+ */
53
+ function readTypeName(name) {
54
+ if (!isString(name)) {
55
+ throw new MalformedSchemaError("type", "every member of the array form must be a string", name);
56
+ }
57
+ if (!isTypeKeyword(name)) {
58
+ throw new MalformedSchemaError("type", `the name "${name}" must be one of the seven type names`, name);
59
+ }
60
+ return name;
61
+ }
62
+ /**
63
+ * The declared names, always as a list. Every departure from the meta-schema
64
+ * is a refusal rather than a name dropped from the list: a dropped name leaves
65
+ * NO type rule at all (the list is what declareTypeRules counts) and also
66
+ * turns permitsNull true, so `{"type":"strig"}` would both stop checking the
67
+ * type and start accepting null.
68
+ *
69
+ * `schema.type` is typed by draft07.types.ts, but the document it came from is
70
+ * plain JSON that nothing has checked, so the value is re-read as `unknown`.
71
+ */
40
72
  export function readDeclaredTypes(schema) {
41
73
  const declared = schema.type;
42
74
  if (declared === undefined)
43
75
  return Object.freeze([]);
44
- const names = isArray(declared) ? declared : [declared];
45
- return Object.freeze(names.filter((name) => isTypeKeyword(name)));
76
+ if (isString(declared))
77
+ return Object.freeze([readTypeName(declared)]);
78
+ if (!isArray(declared)) {
79
+ throw new MalformedSchemaError("type", "the value must be a type name or an array of type names", declared);
80
+ }
81
+ if (declared.length === 0) {
82
+ throw new MalformedSchemaError("type", "the array form must hold at least one name (minItems 1)", declared);
83
+ }
84
+ return Object.freeze(declared.map((name) => readTypeName(name)));
46
85
  }
47
86
  /** True when the document permits an explicit null at this position. */
48
87
  export function permitsNull(schema) {
@@ -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 { NotASchemaError, UnsupportedKeywordError, RefResolutionError, buildFieldEntries, buildFromSchema, fromJsonSchema, isDraft07Schema, listBoundPluginNames, listDraft07Keywords, listFormatNames, } from "../../index";
3
+ export { MalformedSchemaError, NotASchemaError, UnsupportedKeywordError, RefResolutionError, buildFieldEntries, buildFromSchema, fromJsonSchema, isDraft07Schema, listBoundPluginNames, listDraft07Keywords, listFormatNames, } from "../../index";
4
4
  export type { 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.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.NotASchemaError = exports.MalformedSchemaError = 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,6 +15,7 @@ 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, "MalformedSchemaError", { enumerable: true, get: function () { return index_1.MalformedSchemaError; } });
18
19
  Object.defineProperty(exports, "NotASchemaError", { enumerable: true, get: function () { return index_1.NotASchemaError; } });
19
20
  Object.defineProperty(exports, "UnsupportedKeywordError", { enumerable: true, get: function () { return index_1.UnsupportedKeywordError; } });
20
21
  Object.defineProperty(exports, "RefResolutionError", { enumerable: true, get: function () { return index_1.RefResolutionError; } });