|
| 1 | +import { registerSchema, validate } from "@hyperjump/json-schema/draft-2020-12"; |
| 2 | +import { BASIC, compile, getSchema } from "@hyperjump/json-schema/experimental"; |
| 3 | +import * as Instance from "@hyperjump/json-schema/instance/experimental"; |
| 4 | + |
| 5 | +/** |
| 6 | + * @import { OutputUnit } from "@hyperjump/json-schema" |
| 7 | + * @import { AST } from "@hyperjump/json-schema/experimental" |
| 8 | + * @import { JsonNode } from "@hyperjump/json-schema/instance/experimental" |
| 9 | + */ |
| 10 | + |
| 11 | +/** |
| 12 | + * @typedef {{ |
| 13 | + * [instanceLocation: string]: { |
| 14 | + * [keywordUri: string]: { |
| 15 | + * [keywordLocation: string]: boolean | { |
| 16 | + * [schemaLocation: string]: NormalizedOutput |
| 17 | + * } |
| 18 | + * } |
| 19 | + * } |
| 20 | + * }} NormalizedOutput |
| 21 | + */ |
| 22 | + |
| 23 | +/** @type (schemaLocation: string, ast: AST, instance: JsonNode, errorIndex: ErrorIndex) => NormalizedOutput */ |
| 24 | +const evaluateSchema = (schemaLocation, ast, instance, errorIndex) => { |
| 25 | + const instanceLocation = Instance.uri(instance); |
| 26 | + const schemaNode = ast[schemaLocation]; |
| 27 | + |
| 28 | + if (typeof schemaNode === "boolean") { |
| 29 | + return { |
| 30 | + [instanceLocation]: { |
| 31 | + "https://json-schema.org/validation": { |
| 32 | + [schemaLocation]: schemaNode |
| 33 | + } |
| 34 | + } |
| 35 | + }; |
| 36 | + } |
| 37 | + |
| 38 | + /** @type NormalizedOutput */ |
| 39 | + const output = { [instanceLocation]: {} }; |
| 40 | + for (const [keywordUri, keywordLocation, keywordValue] of schemaNode) { |
| 41 | + const keyword = keywordHandlers[keywordUri] ?? {}; |
| 42 | + |
| 43 | + const keywordOutput = keyword.evaluate?.(keywordValue, ast, instance, errorIndex); |
| 44 | + if (keyword.simpleApplicator) { |
| 45 | + for (const subschemaLocation in keywordOutput) { |
| 46 | + mergeOutput(output, keywordOutput[subschemaLocation]); |
| 47 | + } |
| 48 | + } else if (errorIndex[keywordLocation]?.[instanceLocation]) { |
| 49 | + output[instanceLocation][keywordUri] ??= {}; |
| 50 | + output[instanceLocation][keywordUri][keywordLocation] = keywordOutput ?? false; |
| 51 | + } else if (keyword.appliesTo?.(Instance.typeOf(instance)) !== false) { |
| 52 | + output[instanceLocation][keywordUri] ??= {}; |
| 53 | + output[instanceLocation][keywordUri][keywordLocation] = !errorIndex[keywordLocation]?.[instanceLocation]; |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + return output; |
| 58 | +}; |
| 59 | + |
| 60 | +/** @type (a: NormalizedOutput, b: NormalizedOutput) => void */ |
| 61 | +const mergeOutput = (a, b) => { |
| 62 | + for (const instanceLocation in b) { |
| 63 | + for (const keywordUri in b[instanceLocation]) { |
| 64 | + a[instanceLocation] ??= {}; |
| 65 | + a[instanceLocation][keywordUri] ??= {}; |
| 66 | + |
| 67 | + Object.assign(a[instanceLocation][keywordUri], b[instanceLocation][keywordUri]); |
| 68 | + } |
| 69 | + } |
| 70 | +}; |
| 71 | + |
| 72 | +/** |
| 73 | + * @typedef {{ |
| 74 | + * evaluate?(value: any, ast: AST, instance: JsonNode, errorIndex: ErrorIndex): { [schemaLocation: string]: NormalizedOutput } |
| 75 | + * appliesTo?(type: string): boolean; |
| 76 | + * simpleApplicator?: true; |
| 77 | + * }} KeywordHandler |
| 78 | + */ |
| 79 | + |
| 80 | +/** @type Record<string, KeywordHandler> */ |
| 81 | +const keywordHandlers = {}; |
| 82 | + |
| 83 | +keywordHandlers["https://json-schema.org/keyword/anyOf"] = { |
| 84 | + evaluate(/** @type string[] */ anyOf, ast, instance, errorIndex) { |
| 85 | + /** @type {{ [schemaLocation: string]: NormalizedOutput}} */ |
| 86 | + const errors = {}; |
| 87 | + |
| 88 | + for (const schemaLocation of anyOf) { |
| 89 | + errors[schemaLocation] = evaluateSchema(schemaLocation, ast, instance, errorIndex); |
| 90 | + } |
| 91 | + |
| 92 | + return errors; |
| 93 | + } |
| 94 | +}; |
| 95 | + |
| 96 | +keywordHandlers["https://json-schema.org/keyword/oneOf"] = { |
| 97 | + evaluate(/** @type string[] */ oneOf, ast, instance, errorIndex) { |
| 98 | + /** @type Record<string, NormalizedOutput> */ |
| 99 | + const errors = {}; |
| 100 | + |
| 101 | + for (const schemaLocation of oneOf) { |
| 102 | + errors[schemaLocation] = evaluateSchema(schemaLocation, ast, instance, errorIndex); |
| 103 | + } |
| 104 | + |
| 105 | + return errors; |
| 106 | + } |
| 107 | +}; |
| 108 | + |
| 109 | +keywordHandlers["https://json-schema.org/keyword/allOf"] = { |
| 110 | + evaluate(/** @type string[] */ allOf, ast, instance, errorIndex) { |
| 111 | + /** @type Record<string, NormalizedOutput> */ |
| 112 | + const errors = {}; |
| 113 | + |
| 114 | + for (const schemaLocation of allOf) { |
| 115 | + errors[schemaLocation] = evaluateSchema(schemaLocation, ast, instance, errorIndex); |
| 116 | + } |
| 117 | + |
| 118 | + return errors; |
| 119 | + }, |
| 120 | + simpleApplicator: true |
| 121 | +}; |
| 122 | + |
| 123 | +keywordHandlers["https://json-schema.org/keyword/ref"] = { |
| 124 | + evaluate(/** @type string */ ref, ast, instance, errorIndex) { |
| 125 | + return { [ref]: evaluateSchema(ref, ast, instance, errorIndex) }; |
| 126 | + }, |
| 127 | + simpleApplicator: true |
| 128 | +}; |
| 129 | + |
| 130 | +keywordHandlers["https://json-schema.org/keyword/properties"] = { |
| 131 | + evaluate(/** @type Record<string, string> */ properties, ast, instance, errorIndex) { |
| 132 | + /** @type Record<string, NormalizedOutput> */ |
| 133 | + const errors = {}; |
| 134 | + |
| 135 | + for (const propertyName in properties) { |
| 136 | + const propertyNode = Instance.step(propertyName, instance); |
| 137 | + if (!propertyNode) { |
| 138 | + continue; |
| 139 | + } |
| 140 | + |
| 141 | + errors[properties[propertyName]] = evaluateSchema(properties[propertyName], ast, propertyNode, errorIndex); |
| 142 | + } |
| 143 | + |
| 144 | + return errors; |
| 145 | + }, |
| 146 | + simpleApplicator: true |
| 147 | +}; |
| 148 | + |
| 149 | +keywordHandlers["https://json-schema.org/keyword/definitions"] = { |
| 150 | + appliesTo() { |
| 151 | + return false; |
| 152 | + } |
| 153 | +}; |
| 154 | + |
| 155 | +keywordHandlers["https://json-schema.org/keyword/minLength"] = { |
| 156 | + appliesTo(type) { |
| 157 | + return type === "string"; |
| 158 | + } |
| 159 | +}; |
| 160 | + |
| 161 | +keywordHandlers["https://json-schema.org/keyword/minimum"] = { |
| 162 | + appliesTo(type) { |
| 163 | + return type === "number"; |
| 164 | + } |
| 165 | +}; |
| 166 | + |
| 167 | +/** @typedef {Record<string, Record<string, true>>} ErrorIndex */ |
| 168 | + |
| 169 | +/** @type (basicOutput: OutputUnit) => ErrorIndex */ |
| 170 | +const constructErrorIndex = (basicOutput) => { |
| 171 | + /** @type ErrorIndex */ |
| 172 | + const errorIndex = {}; |
| 173 | + |
| 174 | + if (basicOutput.valid) { |
| 175 | + return errorIndex; |
| 176 | + } |
| 177 | + |
| 178 | + for (const error of /** @type OutputUnit[] */ (basicOutput.errors)) { |
| 179 | + errorIndex[error.absoluteKeywordLocation] ??= {}; |
| 180 | + errorIndex[error.absoluteKeywordLocation][error.instanceLocation] = true; |
| 181 | + } |
| 182 | + |
| 183 | + return errorIndex; |
| 184 | +}; |
| 185 | + |
| 186 | +/////////////////////////////////////////////////////////////////////////////// |
| 187 | + |
| 188 | +const subjectUri = "https://example.com/main"; |
| 189 | + |
| 190 | +registerSchema({ |
| 191 | + $schema: "https://json-schema.org/draft/2020-12/schema", |
| 192 | + |
| 193 | + // type: "object", |
| 194 | + // properties: { |
| 195 | + // foo: false, |
| 196 | + // bar: true |
| 197 | + // } |
| 198 | + |
| 199 | + // type: "number", |
| 200 | + // allOf: [ |
| 201 | + // { minimum: 5 }, |
| 202 | + // { minimum: 3 } |
| 203 | + // ] |
| 204 | + |
| 205 | + // allOf: [ |
| 206 | + // true, |
| 207 | + // false |
| 208 | + // ], |
| 209 | + |
| 210 | + type: "object", |
| 211 | + properties: { |
| 212 | + foo: { |
| 213 | + anyOf: [ |
| 214 | + { $ref: "#/$defs/stringSchema" }, |
| 215 | + { $ref: "#/$defs/numberSchema" } |
| 216 | + ] |
| 217 | + }, |
| 218 | + bar: { type: "boolean" } |
| 219 | + }, |
| 220 | + |
| 221 | + $defs: { |
| 222 | + stringSchema: { |
| 223 | + type: "string", |
| 224 | + minLength: 5 |
| 225 | + }, |
| 226 | + numberSchema: { |
| 227 | + type: "number", |
| 228 | + minimum: 10 |
| 229 | + } |
| 230 | + } |
| 231 | +}, subjectUri); |
| 232 | + |
| 233 | +const instance = { foo: 2 }; |
| 234 | + |
| 235 | +const basicOutput = await validate(subjectUri, instance, BASIC); |
| 236 | +const errorIndex = constructErrorIndex(basicOutput); |
| 237 | + |
| 238 | +const schema = await getSchema(subjectUri); |
| 239 | +const { schemaUri, ast } = await compile(schema); |
| 240 | +const value = Instance.fromJs(instance); |
| 241 | +const normalizedOutput = evaluateSchema(schemaUri, ast, value, errorIndex); |
| 242 | +console.log(JSON.stringify(normalizedOutput, null, " ")); |
0 commit comments