@hyperjump/json-schema 0.23.3 → 1.0.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.
- package/LICENSE +1 -1
- package/README.md +459 -120
- package/draft-04/additionalItems.js +29 -0
- package/{lib/keywords → draft-04}/dependencies.js +9 -5
- package/draft-04/exclusiveMaximum.js +5 -0
- package/draft-04/exclusiveMinimum.js +5 -0
- package/draft-04/id.js +1 -0
- package/draft-04/index.d.ts +43 -0
- package/draft-04/index.js +69 -0
- package/draft-04/items.js +35 -0
- package/draft-04/maximum.js +26 -0
- package/draft-04/minimum.js +25 -0
- package/draft-04/ref.js +1 -0
- package/draft-04/schema.js +149 -0
- package/draft-06/contains.js +13 -0
- package/draft-06/index.d.ts +47 -0
- package/draft-06/index.js +66 -0
- package/draft-06/schema.js +154 -0
- package/{lib/draft-07.d.ts → draft-07/index.d.ts} +21 -19
- package/draft-07/index.js +72 -0
- package/draft-07/schema.js +172 -0
- package/{lib/keywords/contains-minContains-maxContains.js → draft-2019-09/contains.js} +14 -7
- package/{lib/draft-2019-09.d.ts → draft-2019-09/index.d.ts} +24 -22
- package/draft-2019-09/index.js +117 -0
- package/draft-2019-09/meta/applicator.js +55 -0
- package/draft-2019-09/meta/content.js +17 -0
- package/draft-2019-09/meta/core.js +57 -0
- package/draft-2019-09/meta/format.js +14 -0
- package/draft-2019-09/meta/meta-data.js +37 -0
- package/draft-2019-09/meta/validation.js +98 -0
- package/draft-2019-09/recursiveAnchor.js +1 -0
- package/draft-2019-09/recursiveRef.js +19 -0
- package/draft-2019-09/schema.js +42 -0
- package/draft-2020-12/dynamicAnchor.js +1 -0
- package/draft-2020-12/dynamicRef.js +35 -0
- package/draft-2020-12/index.d.ts +66 -0
- package/draft-2020-12/index.js +124 -0
- package/draft-2020-12/meta/applicator.js +46 -0
- package/draft-2020-12/meta/content.js +14 -0
- package/draft-2020-12/meta/core.js +54 -0
- package/draft-2020-12/meta/format-annotation.js +11 -0
- package/draft-2020-12/meta/format-assertion.js +11 -0
- package/draft-2020-12/meta/meta-data.js +34 -0
- package/draft-2020-12/meta/unevaluated.js +12 -0
- package/draft-2020-12/meta/validation.js +95 -0
- package/draft-2020-12/schema.js +44 -0
- package/lib/common.d.ts +5 -1
- package/lib/common.js +80 -9
- package/lib/configuration.d.ts +9 -0
- package/lib/configuration.js +18 -0
- package/lib/core.d.ts +48 -0
- package/lib/core.js +102 -0
- package/lib/experimental.d.ts +8 -0
- package/lib/experimental.js +4 -0
- package/lib/fetch.browser.js +1 -0
- package/lib/fetch.js +19 -0
- package/lib/index.d.ts +11 -42
- package/lib/index.js +130 -23
- package/lib/instance.d.ts +75 -0
- package/lib/instance.js +58 -0
- package/lib/invalid-schema-error.d.ts +8 -0
- package/lib/invalid-schema-error.js +7 -0
- package/lib/keywords/additionalProperties.js +19 -17
- package/lib/keywords/allOf.js +10 -7
- package/lib/keywords/anchor.js +1 -0
- package/lib/keywords/anyOf.js +10 -7
- package/lib/keywords/comment.js +4 -0
- package/lib/keywords/const.js +6 -3
- package/lib/keywords/contains.js +48 -5
- package/lib/keywords/contentEncoding.js +4 -0
- package/lib/keywords/contentMediaType.js +4 -0
- package/lib/keywords/contentSchema.js +4 -0
- package/lib/keywords/default.js +4 -0
- package/lib/keywords/definitions.js +7 -4
- package/lib/keywords/dependentRequired.js +6 -3
- package/lib/keywords/dependentSchemas.js +10 -6
- package/lib/keywords/deprecated.js +4 -0
- package/lib/keywords/description.js +4 -0
- package/lib/keywords/dynamicAnchor.js +1 -0
- package/lib/keywords/dynamicRef.js +13 -17
- package/lib/keywords/else.js +16 -11
- package/lib/keywords/enum.js +6 -3
- package/lib/keywords/examples.js +4 -0
- package/lib/keywords/exclusiveMaximum.js +5 -2
- package/lib/keywords/exclusiveMinimum.js +5 -2
- package/lib/keywords/format.js +4 -0
- package/lib/keywords/id.js +1 -0
- package/lib/keywords/if.js +8 -6
- package/lib/keywords/items.js +17 -19
- package/lib/keywords/maxContains.js +4 -0
- package/lib/keywords/maxItems.js +5 -2
- package/lib/keywords/maxLength.js +5 -2
- package/lib/keywords/maxProperties.js +5 -2
- package/lib/keywords/maximum.js +5 -2
- package/lib/keywords/meta-data.js +4 -0
- package/lib/keywords/minContains.js +4 -0
- package/lib/keywords/minItems.js +5 -2
- package/lib/keywords/minLength.js +5 -2
- package/lib/keywords/minProperties.js +5 -2
- package/lib/keywords/minimum.js +5 -2
- package/lib/keywords/multipleOf.js +5 -2
- package/lib/keywords/not.js +6 -4
- package/lib/keywords/oneOf.js +9 -6
- package/lib/keywords/pattern.js +5 -2
- package/lib/keywords/patternProperties.js +9 -5
- package/lib/keywords/prefixItems.js +28 -0
- package/lib/keywords/properties.js +11 -6
- package/lib/keywords/propertyDependencies.js +49 -0
- package/lib/keywords/propertyNames.js +7 -4
- package/lib/keywords/readOnly.js +4 -0
- package/lib/keywords/ref.js +9 -6
- package/lib/keywords/requireAllExcept.js +24 -0
- package/lib/keywords/required.js +5 -2
- package/lib/keywords/then.js +16 -11
- package/lib/keywords/title.js +4 -0
- package/lib/keywords/type.js +5 -2
- package/lib/keywords/unevaluatedItems.js +9 -5
- package/lib/keywords/unevaluatedProperties.js +9 -5
- package/lib/keywords/uniqueItems.js +6 -3
- package/lib/keywords/validation.js +123 -0
- package/lib/keywords/vocabulary.js +1 -0
- package/lib/keywords/writeOnly.js +4 -0
- package/lib/keywords.d.ts +19 -0
- package/lib/keywords.js +59 -0
- package/lib/media-types.d.ts +9 -0
- package/lib/media-types.js +26 -0
- package/lib/pubsub.js +42 -0
- package/lib/reference.d.ts +11 -0
- package/lib/reference.js +11 -0
- package/lib/schema.d.ts +64 -0
- package/lib/schema.js +308 -0
- package/package.json +31 -22
- package/{lib/draft-2020-12.d.ts → stable/index.d.ts} +26 -24
- package/stable/index.js +118 -0
- package/stable/meta/applicator.js +49 -0
- package/stable/meta/content.js +12 -0
- package/stable/meta/core.js +49 -0
- package/stable/meta/format-annotation.js +10 -0
- package/stable/meta/format-assertion.js +10 -0
- package/stable/meta/meta-data.js +16 -0
- package/stable/meta/unevaluated.js +11 -0
- package/stable/meta/validation.js +67 -0
- package/stable/validation.js +24 -0
- package/dist/json-schema-amd.js +0 -6602
- package/dist/json-schema-amd.js.map +0 -1
- package/dist/json-schema-amd.min.js +0 -3
- package/dist/json-schema-amd.min.js.map +0 -1
- package/dist/json-schema-cjs.js +0 -6600
- package/dist/json-schema-cjs.js.map +0 -1
- package/dist/json-schema-cjs.min.js +0 -3
- package/dist/json-schema-cjs.min.js.map +0 -1
- package/dist/json-schema-esm.js +0 -6596
- package/dist/json-schema-esm.js.map +0 -1
- package/dist/json-schema-esm.min.js +0 -3
- package/dist/json-schema-esm.min.js.map +0 -1
- package/dist/json-schema-iife.js +0 -6605
- package/dist/json-schema-iife.js.map +0 -1
- package/dist/json-schema-iife.min.js +0 -3
- package/dist/json-schema-iife.min.js.map +0 -1
- package/dist/json-schema-system.js +0 -6603
- package/dist/json-schema-system.js.map +0 -1
- package/dist/json-schema-system.min.js +0 -3
- package/dist/json-schema-system.min.js.map +0 -1
- package/dist/json-schema-umd.js +0 -6606
- package/dist/json-schema-umd.js.map +0 -1
- package/dist/json-schema-umd.min.js +0 -3
- package/dist/json-schema-umd.min.js.map +0 -1
- package/lib/draft-04.d.ts +0 -41
- package/lib/draft-04.js +0 -46
- package/lib/draft-06.d.ts +0 -45
- package/lib/draft-06.js +0 -51
- package/lib/draft-07.js +0 -55
- package/lib/draft-2019-09.js +0 -92
- package/lib/draft-2020-12.js +0 -103
- package/lib/index.mjs +0 -19
- package/lib/keywords/additionalItems.js +0 -27
- package/lib/keywords/additionalItems6.js +0 -23
- package/lib/keywords/additionalProperties6.js +0 -28
- package/lib/keywords/index.js +0 -53
- package/lib/keywords/items202012.js +0 -23
- package/lib/keywords/maximum-exclusiveMaximum.js +0 -20
- package/lib/keywords/minimum-exclusiveMinimum.js +0 -20
- package/lib/keywords/tupleItems.js +0 -24
- package/meta/draft/2019-09/meta/applicator.js +0 -55
- package/meta/draft/2019-09/meta/content.js +0 -17
- package/meta/draft/2019-09/meta/core.js +0 -57
- package/meta/draft/2019-09/meta/format.js +0 -14
- package/meta/draft/2019-09/meta/meta-data.js +0 -37
- package/meta/draft/2019-09/meta/validation.js +0 -98
- package/meta/draft/2019-09/schema.js +0 -42
- package/meta/draft/2020-12/meta/applicator.js +0 -49
- package/meta/draft/2020-12/meta/content.js +0 -17
- package/meta/draft/2020-12/meta/core.js +0 -57
- package/meta/draft/2020-12/meta/format-annotation.js +0 -14
- package/meta/draft/2020-12/meta/format-assertion.js +0 -14
- package/meta/draft/2020-12/meta/meta-data.js +0 -37
- package/meta/draft/2020-12/meta/unevaluated.js +0 -15
- package/meta/draft/2020-12/meta/validation.js +0 -98
- package/meta/draft/2020-12/schema.js +0 -44
- package/meta/draft-04/schema.js +0 -149
- package/meta/draft-06/schema.js +0 -154
- package/meta/draft-07/schema.js +0 -172
package/lib/keywords.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import metaData from "./keywords/meta-data.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const _keywords = {};
|
|
5
|
+
export const getKeyword = (id) => _keywords[id] || id.startsWith("https://json-schema.org/keyword/unknown#") && { id, ...metaData };
|
|
6
|
+
|
|
7
|
+
export const addKeyword = (keywordHandler) => {
|
|
8
|
+
_keywords[keywordHandler.id] = {
|
|
9
|
+
collectEvaluatedItems: (keywordValue, instance, ast, dynamicAnchors, isTop) => keywordHandler.interpret(keywordValue, instance, ast, dynamicAnchors, isTop) && new Set(),
|
|
10
|
+
collectEvaluatedProperties: (keywordValue, instance, ast, dynamicAnchors, isTop) => keywordHandler.interpret(keywordValue, instance, ast, dynamicAnchors, isTop) && [],
|
|
11
|
+
...keywordHandler
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const _vocabularies = {};
|
|
16
|
+
export const defineVocabulary = (id, keywords) => {
|
|
17
|
+
_vocabularies[id] = keywords;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const _dialects = {};
|
|
21
|
+
const _allowUnknownKeywords = {};
|
|
22
|
+
export const getKeywordId = (dialectId, keyword) => _dialects[dialectId]?.[keyword]
|
|
23
|
+
|| _allowUnknownKeywords[dialectId] && `https://json-schema.org/keyword/unknown#${keyword}`;
|
|
24
|
+
export const getKeywordName = (dialectId, keywordId) => {
|
|
25
|
+
for (const keyword in _dialects[dialectId]) {
|
|
26
|
+
if (_dialects[dialectId][keyword] === keywordId) {
|
|
27
|
+
return keyword;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const hasDialect = (dialectId) => dialectId in _dialects;
|
|
33
|
+
|
|
34
|
+
export const loadDialect = (dialectId, dialect, allowUnknownKeywords = false) => {
|
|
35
|
+
_allowUnknownKeywords[dialectId] = allowUnknownKeywords;
|
|
36
|
+
|
|
37
|
+
_dialects[dialectId] = {};
|
|
38
|
+
Object.entries(dialect)
|
|
39
|
+
.forEach(([vocabularyId, isRequired]) => {
|
|
40
|
+
if (vocabularyId in _vocabularies) {
|
|
41
|
+
Object.entries(_vocabularies[vocabularyId])
|
|
42
|
+
.forEach(([keyword, keywordId]) => {
|
|
43
|
+
if (!(keywordId in _keywords)) {
|
|
44
|
+
if (isRequired) {
|
|
45
|
+
delete _dialects[dialectId];
|
|
46
|
+
throw Error(`The '${keywordId}' keyword is not supported. This keyword was included in the '${vocabularyId}' vocabulary which is required by the '${dialectId}' dialect.`);
|
|
47
|
+
} else {
|
|
48
|
+
// Allow keyword to be ignored
|
|
49
|
+
keywordId = `https://json-schema.org/keyword/unknown#${keyword}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
_dialects[dialectId][keyword] = keywordId;
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
delete _dialects[dialectId];
|
|
56
|
+
throw Error(`Unrecognized vocabulary: ${vocabularyId}. You can define this vocabulary with the 'defineVocabulary' function.`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SchemaObject } from "./schema.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const addMediaTypePlugin: (contentType: string, plugin: MediaTypePlugin) => void;
|
|
5
|
+
|
|
6
|
+
export type MediaTypePlugin = {
|
|
7
|
+
parse: (response: Response, mediaTypeParameters: { [parameter: string]: string }) => Promise<[SchemaObject, string | undefined]>;
|
|
8
|
+
matcher: (path: string) => boolean;
|
|
9
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import contentTypeParser from "content-type";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const mediaTypePlugins = {};
|
|
5
|
+
|
|
6
|
+
export const addMediaTypePlugin = (contentType, plugin) => {
|
|
7
|
+
mediaTypePlugins[contentType] = plugin;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const parse = (response) => {
|
|
11
|
+
const contentType = contentTypeParser.parse(response.headers.get("content-type"));
|
|
12
|
+
if (!(contentType.type in mediaTypePlugins)) {
|
|
13
|
+
throw Error(`${response.url} is not a schema. Found a document with media type: ${contentType.type}`);
|
|
14
|
+
}
|
|
15
|
+
return mediaTypePlugins[contentType.type].parse(response, contentType.parameters);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getContentType = (path) => {
|
|
19
|
+
for (const contentType in mediaTypePlugins) {
|
|
20
|
+
if (mediaTypePlugins[contentType].matcher(path)) {
|
|
21
|
+
return contentType;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return "application/octet-stream";
|
|
26
|
+
};
|
package/lib/pubsub.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const subscriptions = {};
|
|
2
|
+
let uid = 0;
|
|
3
|
+
|
|
4
|
+
export const subscribe = (message, fn) => {
|
|
5
|
+
if (!(message in subscriptions)) {
|
|
6
|
+
subscriptions[message] = {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const subscriptionId = `pubsub_subscription_${uid++}`;
|
|
10
|
+
subscriptions[message][subscriptionId] = fn;
|
|
11
|
+
|
|
12
|
+
return subscriptionId;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const unsubscribe = (message, token) => {
|
|
16
|
+
delete subscriptions[message][token];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const publish = (message, data) => {
|
|
20
|
+
for (const subscribedMessage in subscriptions) {
|
|
21
|
+
if (subscribedMessage === message || message.startsWith(`${subscribedMessage}.`)) {
|
|
22
|
+
for (const subscriptionId in subscriptions[subscribedMessage]) {
|
|
23
|
+
subscriptions[subscribedMessage][subscriptionId](message, data);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const publishAsync = async (message, data) => {
|
|
30
|
+
const promises = [];
|
|
31
|
+
for (const subscribedMessage in subscriptions) {
|
|
32
|
+
if (subscribedMessage === message || message.startsWith(`${subscribedMessage}.`)) {
|
|
33
|
+
for (const subscriptionId in subscriptions[subscribedMessage]) {
|
|
34
|
+
promises.push(subscriptions[message][subscriptionId](message, data));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await Promise.all(promises);
|
|
40
|
+
|
|
41
|
+
return;
|
|
42
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const cons: (href: string, value: unknown) => Ref;
|
|
2
|
+
export const isReference: (ref: unknown) => ref is Ref;
|
|
3
|
+
export const href: (ref: Ref) => string;
|
|
4
|
+
export const value: <A>(ref: Ref) => A;
|
|
5
|
+
|
|
6
|
+
declare const $__value: unique symbol;
|
|
7
|
+
declare const $__href: unique symbol;
|
|
8
|
+
export type Ref = {
|
|
9
|
+
[$__href]: string;
|
|
10
|
+
[$__value]: unknown;
|
|
11
|
+
};
|
package/lib/reference.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const $__value = Symbol("$__value");
|
|
2
|
+
const $__href = Symbol("$__href");
|
|
3
|
+
|
|
4
|
+
export const cons = (href, value) => Object.freeze({
|
|
5
|
+
[$__href]: href,
|
|
6
|
+
[$__value]: value
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const isReference = (ref) => ref && ref[$__href] !== undefined;
|
|
10
|
+
export const href = (ref) => ref[$__href];
|
|
11
|
+
export const value = (ref) => ref[$__value];
|
package/lib/schema.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { JsonType } from "./common.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const add: <A extends SchemaObject | boolean>(schema: A, url?: string, defaultSchemaVersion?: string) => string;
|
|
5
|
+
export const get: (url: string, context?: SchemaDocument) => Promise<SchemaDocument>;
|
|
6
|
+
export const markValidated: (id: string) => void;
|
|
7
|
+
export const uri: (doc: SchemaDocument) => string;
|
|
8
|
+
export const value: <A extends SchemaFragment>(doc: SchemaDocument<A>) => A;
|
|
9
|
+
export const typeOf: (
|
|
10
|
+
(doc: SchemaDocument, type: "null") => doc is SchemaDocument<null>
|
|
11
|
+
) & (
|
|
12
|
+
(doc: SchemaDocument, type: "boolean") => doc is SchemaDocument<boolean>
|
|
13
|
+
) & (
|
|
14
|
+
(doc: SchemaDocument, type: "object") => doc is SchemaDocument<SchemaObject>
|
|
15
|
+
) & (
|
|
16
|
+
(doc: SchemaDocument, type: "array") => doc is SchemaDocument<SchemaFragment[]>
|
|
17
|
+
) & (
|
|
18
|
+
(doc: SchemaDocument, type: "number" | "integer") => doc is SchemaDocument<number>
|
|
19
|
+
) & (
|
|
20
|
+
(doc: SchemaDocument, type: "string") => doc is SchemaDocument<string>
|
|
21
|
+
) & (
|
|
22
|
+
(doc: SchemaDocument, type: JsonType) => boolean
|
|
23
|
+
) & (
|
|
24
|
+
(doc: SchemaDocument) => (type: JsonType) => boolean
|
|
25
|
+
);
|
|
26
|
+
export const has: (key: string, doc: SchemaDocument) => boolean;
|
|
27
|
+
export const step: (key: string, doc: SchemaDocument) => Promise<SchemaDocument>;
|
|
28
|
+
export const keys: (doc: SchemaDocument) => string[];
|
|
29
|
+
export const entries: (doc: SchemaDocument) => Promise<SchemaEntry[]>;
|
|
30
|
+
export const map: (
|
|
31
|
+
<A>(fn: MapFn<Promise<A> | A>, doc: SchemaDocument) => Promise<A[]>
|
|
32
|
+
) & (
|
|
33
|
+
<A>(fn: MapFn<Promise<A> | A>) => (doc: SchemaDocument) => Promise<A[]>
|
|
34
|
+
);
|
|
35
|
+
export const length: (doc: SchemaDocument) => number;
|
|
36
|
+
export const toSchema: (doc: SchemaDocument, options: ToSchemaOptions) => SchemaObject;
|
|
37
|
+
|
|
38
|
+
type MapFn<A> = (element: SchemaDocument, index: number) => A;
|
|
39
|
+
export type SchemaEntry = [string, SchemaDocument];
|
|
40
|
+
|
|
41
|
+
export type ToSchemaOptions = {
|
|
42
|
+
parentId?: string;
|
|
43
|
+
parentDialect?: string;
|
|
44
|
+
includeEmbedded?: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type SchemaDocument<A extends SchemaFragment = SchemaFragment> = {
|
|
48
|
+
id: string;
|
|
49
|
+
dialectId: string;
|
|
50
|
+
pointer: string;
|
|
51
|
+
schema: SchemaObject;
|
|
52
|
+
value: A;
|
|
53
|
+
anchors: Anchors;
|
|
54
|
+
dynamicAnchors: Anchors;
|
|
55
|
+
vocabulary: Record<string, boolean>;
|
|
56
|
+
validated: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type SchemaFragment = string | number | boolean | null | SchemaObject | SchemaFragment[];
|
|
60
|
+
export type SchemaObject = { // eslint-disable-line @typescript-eslint/consistent-indexed-object-style
|
|
61
|
+
[keyword: string]: SchemaFragment;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type Anchors = Record<string, string>;
|
package/lib/schema.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import curry from "just-curry-it";
|
|
2
|
+
import * as Pact from "@hyperjump/pact";
|
|
3
|
+
import * as Json from "@hyperjump/json";
|
|
4
|
+
import * as JsonPointer from "@hyperjump/json-pointer";
|
|
5
|
+
import { jsonTypeOf, resolveUrl, urlFragment, pathRelative } from "./common.js";
|
|
6
|
+
import fetch from "./fetch.js";
|
|
7
|
+
import * as Keywords from "./keywords.js";
|
|
8
|
+
import * as MediaTypes from "./media-types.js";
|
|
9
|
+
import * as Reference from "./reference.js";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// Schema Management
|
|
13
|
+
const schemaStore = {};
|
|
14
|
+
const schemaStoreAlias = {};
|
|
15
|
+
|
|
16
|
+
const defaultDialectId = "https://json-schema.org/validation";
|
|
17
|
+
|
|
18
|
+
export const add = (schema, url = undefined, contextDialectId = undefined) => {
|
|
19
|
+
schema = JSON.parse(JSON.stringify(schema));
|
|
20
|
+
|
|
21
|
+
// Dialect / JSON Schema Version
|
|
22
|
+
const dialectId = resolveUrl("", schema.$schema || contextDialectId || defaultDialectId);
|
|
23
|
+
delete schema.$schema;
|
|
24
|
+
|
|
25
|
+
if (!Keywords.hasDialect(dialectId)) {
|
|
26
|
+
throw Error(`Encountered unknown dialect '${dialectId}'`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Identifiers
|
|
30
|
+
const idToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/id")
|
|
31
|
+
|| Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/draft-04/id");
|
|
32
|
+
const internalUrl = resolveUrl(schema[idToken] || url, url);
|
|
33
|
+
const id = resolveUrl("", internalUrl);
|
|
34
|
+
delete schema[idToken];
|
|
35
|
+
if (url) {
|
|
36
|
+
const externalId = resolveUrl("", url);
|
|
37
|
+
schemaStoreAlias[externalId] = id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Vocabulary
|
|
41
|
+
const vocabularyToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/vocabulary");
|
|
42
|
+
if (jsonTypeOf(schema[vocabularyToken], "object")) {
|
|
43
|
+
const allowUnknownKeywords = schema[vocabularyToken]["https://json-schema.org/draft/2019-09/vocab/core"]
|
|
44
|
+
|| schema[vocabularyToken]["https://json-schema.org/draft/2020-12/vocab/core"];
|
|
45
|
+
|
|
46
|
+
Keywords.loadDialect(id, schema[vocabularyToken], allowUnknownKeywords);
|
|
47
|
+
delete schema[vocabularyToken];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dynamicAnchors = {};
|
|
51
|
+
|
|
52
|
+
// Recursive anchor
|
|
53
|
+
const recursiveAnchorToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/draft-2019-09/recursiveAnchor");
|
|
54
|
+
if (schema[recursiveAnchorToken] === true) {
|
|
55
|
+
dynamicAnchors[""] = `${id}#`;
|
|
56
|
+
}
|
|
57
|
+
delete schema[recursiveAnchorToken];
|
|
58
|
+
|
|
59
|
+
// Store Schema
|
|
60
|
+
const anchors = { "": "" };
|
|
61
|
+
schemaStore[id] = {
|
|
62
|
+
id: id,
|
|
63
|
+
dialectId: dialectId,
|
|
64
|
+
schema: processSchema(schema, id, dialectId, JsonPointer.nil, anchors, dynamicAnchors),
|
|
65
|
+
anchors: anchors,
|
|
66
|
+
dynamicAnchors: dynamicAnchors,
|
|
67
|
+
validated: false
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return id;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const processSchema = (subject, id, dialectId, pointer, anchors, dynamicAnchors) => {
|
|
74
|
+
if (jsonTypeOf(subject, "object")) {
|
|
75
|
+
// Legacy id
|
|
76
|
+
const legacyIdToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/draft-04/id");
|
|
77
|
+
if (typeof subject[legacyIdToken] === "string") {
|
|
78
|
+
if (subject[legacyIdToken][0] === "#") {
|
|
79
|
+
const anchor = decodeURIComponent(subject[legacyIdToken].slice(1));
|
|
80
|
+
anchors[anchor] = pointer;
|
|
81
|
+
} else {
|
|
82
|
+
delete subject[legacyIdToken].$schema;
|
|
83
|
+
subject[legacyIdToken] = resolveUrl(subject[legacyIdToken], id);
|
|
84
|
+
add(subject, undefined, dialectId);
|
|
85
|
+
return Reference.cons(subject[legacyIdToken], subject);
|
|
86
|
+
}
|
|
87
|
+
delete subject[legacyIdToken];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Embedded Schema
|
|
91
|
+
const embeddedDialectId = typeof subject.$schema === "string" ? resolveUrl("", subject.$schema) : dialectId;
|
|
92
|
+
const idToken = Keywords.getKeywordName(embeddedDialectId, "https://json-schema.org/keyword/id");
|
|
93
|
+
if (typeof subject[idToken] === "string") {
|
|
94
|
+
subject[idToken] = resolveUrl(subject[idToken], id);
|
|
95
|
+
add(subject, undefined, dialectId);
|
|
96
|
+
return Reference.cons(subject[idToken], subject);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Legacy dynamic anchor
|
|
100
|
+
const legacyDynamicAnchorToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/draft-2020-12/dynamicAnchor");
|
|
101
|
+
if (typeof subject[legacyDynamicAnchorToken] === "string") {
|
|
102
|
+
dynamicAnchors[subject[legacyDynamicAnchorToken]] = `${id}#${encodeURI(pointer)}`;
|
|
103
|
+
anchors[subject[legacyDynamicAnchorToken]] = pointer;
|
|
104
|
+
delete subject[legacyDynamicAnchorToken];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const dynamicAnchorToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/dynamicAnchor");
|
|
108
|
+
if (typeof subject[dynamicAnchorToken] === "string") {
|
|
109
|
+
dynamicAnchors[subject[dynamicAnchorToken]] = `${id}#${encodeURI(pointer)}`;
|
|
110
|
+
delete subject[dynamicAnchorToken];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const anchorToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/anchor");
|
|
114
|
+
if (typeof subject[anchorToken] === "string") {
|
|
115
|
+
anchors[subject[anchorToken]] = pointer;
|
|
116
|
+
delete subject[anchorToken];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Legacy $ref
|
|
120
|
+
const jrefToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/draft-04/ref");
|
|
121
|
+
if (typeof subject[jrefToken] === "string") {
|
|
122
|
+
return Reference.cons(subject[jrefToken], subject);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const key in subject) {
|
|
126
|
+
subject[key] = processSchema(subject[key], id, dialectId, JsonPointer.append(key, pointer), anchors, dynamicAnchors);
|
|
127
|
+
}
|
|
128
|
+
} else if (Array.isArray(subject)) {
|
|
129
|
+
for (let index = 0; index < subject.length; index++) {
|
|
130
|
+
subject[index] = processSchema(subject[index], id, dialectId, JsonPointer.append(index, pointer), anchors, dynamicAnchors);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return subject;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const hasStoredSchema = (id) => id in schemaStore || id in schemaStoreAlias;
|
|
138
|
+
const getStoredSchema = (id) => schemaStore[schemaStoreAlias[id]] || schemaStore[id];
|
|
139
|
+
|
|
140
|
+
export const markValidated = (id) => {
|
|
141
|
+
schemaStore[id].validated = true;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Schema Retrieval
|
|
145
|
+
const nil = Object.freeze({
|
|
146
|
+
id: undefined,
|
|
147
|
+
dialectId: undefined,
|
|
148
|
+
pointer: JsonPointer.nil,
|
|
149
|
+
schema: undefined,
|
|
150
|
+
value: undefined,
|
|
151
|
+
anchors: {},
|
|
152
|
+
dynamicAnchors: {},
|
|
153
|
+
validated: true
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export const get = async (url, contextDoc = nil) => {
|
|
157
|
+
const resolvedUrl = resolveUrl(url, uri(contextDoc));
|
|
158
|
+
const id = resolveUrl("", resolvedUrl);
|
|
159
|
+
const fragment = urlFragment(resolvedUrl);
|
|
160
|
+
|
|
161
|
+
if (!hasStoredSchema(id)) {
|
|
162
|
+
const response = await fetch(id, { headers: { Accept: "application/schema+json" } });
|
|
163
|
+
if (response.status >= 400) {
|
|
164
|
+
await response.text(); // Sometimes node hangs without this hack
|
|
165
|
+
throw Error(`Failed to retrieve schema with id: ${id}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const [schema, contextDialectId] = await MediaTypes.parse(response);
|
|
169
|
+
|
|
170
|
+
// Try to determine the dialect from the meta-schema if it isn't already known
|
|
171
|
+
const dialectId = resolveUrl("", schema.$schema || contextDialectId || defaultDialectId);
|
|
172
|
+
if (!Keywords.hasDialect(dialectId) && !hasStoredSchema(dialectId)) {
|
|
173
|
+
await get(dialectId);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
add(schema, id, contextDialectId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const storedSchema = getStoredSchema(id);
|
|
180
|
+
const pointer = fragment[0] !== "/" ? getAnchorPointer(storedSchema, fragment) : fragment;
|
|
181
|
+
const doc = Object.freeze({
|
|
182
|
+
...storedSchema,
|
|
183
|
+
pointer: pointer,
|
|
184
|
+
value: JsonPointer.get(pointer, storedSchema.schema)
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return followReferences(doc);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const followReferences = (doc) => Reference.isReference(doc.value) ? get(Reference.href(doc.value), doc) : doc;
|
|
191
|
+
|
|
192
|
+
const getAnchorPointer = (schema, fragment) => {
|
|
193
|
+
if (!(fragment in schema.anchors)) {
|
|
194
|
+
throw Error(`No such anchor '${encodeURI(schema.id)}#${encodeURI(fragment)}'`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return schema.anchors[fragment];
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Utility Functions
|
|
201
|
+
export const uri = (doc) => doc.id ? `${doc.id}#${encodeURI(doc.pointer)}` : undefined;
|
|
202
|
+
export const value = (doc) => Reference.isReference(doc.value) ? Reference.value(doc.value) : doc.value;
|
|
203
|
+
export const has = (key, doc) => key in value(doc);
|
|
204
|
+
export const typeOf = (doc, type) => jsonTypeOf(value(doc), type);
|
|
205
|
+
|
|
206
|
+
export const step = (key, doc) => {
|
|
207
|
+
const storedSchema = getStoredSchema(doc.id);
|
|
208
|
+
const nextDoc = Object.freeze({
|
|
209
|
+
...doc,
|
|
210
|
+
pointer: JsonPointer.append(`${key}`, doc.pointer),
|
|
211
|
+
value: value(doc)[key],
|
|
212
|
+
validated: storedSchema.validated
|
|
213
|
+
});
|
|
214
|
+
return followReferences(nextDoc);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const keys = (doc) => Object.keys(value(doc));
|
|
218
|
+
|
|
219
|
+
export const entries = (doc) => Pact.pipeline([
|
|
220
|
+
value,
|
|
221
|
+
Object.keys,
|
|
222
|
+
Pact.map(async (key) => [key, await step(key, doc)]),
|
|
223
|
+
Pact.all
|
|
224
|
+
], doc);
|
|
225
|
+
|
|
226
|
+
export const map = curry((fn, doc) => Pact.pipeline([
|
|
227
|
+
value,
|
|
228
|
+
Pact.map(async (item, ndx) => fn(await step(ndx, doc), ndx)),
|
|
229
|
+
Pact.all
|
|
230
|
+
], doc));
|
|
231
|
+
|
|
232
|
+
export const length = (doc) => value(doc).length;
|
|
233
|
+
|
|
234
|
+
const toSchemaDefaultOptions = {
|
|
235
|
+
parentId: "",
|
|
236
|
+
parentDialect: "",
|
|
237
|
+
includeEmbedded: true
|
|
238
|
+
};
|
|
239
|
+
export const toSchema = (schemaDoc, options = {}) => {
|
|
240
|
+
const fullOptions = { ...toSchemaDefaultOptions, ...options };
|
|
241
|
+
|
|
242
|
+
const idToken = Keywords.getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/id");
|
|
243
|
+
const anchorToken = Keywords.getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/anchor");
|
|
244
|
+
const dynamicAnchorToken = Keywords.getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/dynamicAnchor");
|
|
245
|
+
const legacyDynamicAnchorToken = Keywords.getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/draft-2020-12/dynamicAnchor");
|
|
246
|
+
const recursiveAnchorToken = Keywords.getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/recursiveAnchor");
|
|
247
|
+
|
|
248
|
+
const anchors = {};
|
|
249
|
+
for (const anchor in schemaDoc.anchors) {
|
|
250
|
+
if (anchor !== "" && !schemaDoc.dynamicAnchors[anchor]) {
|
|
251
|
+
anchors[schemaDoc.anchors[anchor]] = anchor;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const dynamicAnchors = {};
|
|
256
|
+
for (const anchor in schemaDoc.dynamicAnchors) {
|
|
257
|
+
const pointer = urlFragment(schemaDoc.dynamicAnchors[anchor]);
|
|
258
|
+
dynamicAnchors[pointer] = anchor;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const schema = JSON.parse(Json.stringify(schemaDoc.schema, (key, value, pointer) => {
|
|
262
|
+
if (Reference.isReference(value)) {
|
|
263
|
+
const refValue = Reference.value(value);
|
|
264
|
+
if (!fullOptions.includeEmbedded && idToken in refValue) {
|
|
265
|
+
return;
|
|
266
|
+
} else {
|
|
267
|
+
return Reference.value(value);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
if (pointer in anchors) {
|
|
271
|
+
value = { [anchorToken]: anchors[pointer], ...value };
|
|
272
|
+
}
|
|
273
|
+
if (pointer in dynamicAnchors) {
|
|
274
|
+
if (dynamicAnchorToken) {
|
|
275
|
+
value = { [dynamicAnchorToken]: dynamicAnchors[pointer], ...value };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Legacy dynamic anchor
|
|
279
|
+
if (legacyDynamicAnchorToken) {
|
|
280
|
+
value = { [legacyDynamicAnchorToken]: dynamicAnchors[pointer], ...value };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Recursive anchor
|
|
284
|
+
if (recursiveAnchorToken) {
|
|
285
|
+
value = { [recursiveAnchorToken]: true, ...value };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return value;
|
|
289
|
+
}
|
|
290
|
+
}));
|
|
291
|
+
|
|
292
|
+
const id = relativeUri(fullOptions.parentId, schemaDoc.id);
|
|
293
|
+
const dialect = fullOptions.parentDialect === schemaDoc.dialectId ? "" : schemaDoc.dialectId;
|
|
294
|
+
return {
|
|
295
|
+
...(id && { [idToken]: id }),
|
|
296
|
+
...(dialect && { $schema: dialect }),
|
|
297
|
+
...schema
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const relativeUri = (from, to) => {
|
|
302
|
+
if (to.startsWith("file://")) {
|
|
303
|
+
const pathToSchema = from.slice(7, from.lastIndexOf("/"));
|
|
304
|
+
return from === "" ? "" : pathRelative(pathToSchema, to.slice(7));
|
|
305
|
+
} else {
|
|
306
|
+
return to;
|
|
307
|
+
}
|
|
308
|
+
};
|
package/package.json
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperjump/json-schema",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A JSON Schema
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A JSON Schema validator with support for custom keywords, vocabularies, and dialects",
|
|
5
|
+
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
".": "./stable/index.js",
|
|
8
|
+
"./draft-04": "./draft-04/index.js",
|
|
9
|
+
"./draft-06": "./draft-06/index.js",
|
|
10
|
+
"./draft-07": "./draft-07/index.js",
|
|
11
|
+
"./draft-2019-09": "./draft-2019-09/index.js",
|
|
12
|
+
"./draft-2020-12": "./draft-2020-12/index.js",
|
|
13
|
+
"./experimental": "./lib/experimental.js",
|
|
14
|
+
"./schema/experimental": "./lib/schema.js",
|
|
15
|
+
"./instance/experimental": "./lib/instance.js"
|
|
16
|
+
},
|
|
17
|
+
"browser": {
|
|
18
|
+
"./lib/fetch.js": "./lib/fetch.browser.js"
|
|
9
19
|
},
|
|
10
20
|
"scripts": {
|
|
11
21
|
"clean": "xargs -a .gitignore rm -rf",
|
|
12
22
|
"lint": "eslint lib",
|
|
13
|
-
"test": "mocha
|
|
14
|
-
"build": "rollup --config rollup.config.js",
|
|
15
|
-
"prepublishOnly": "npm run build",
|
|
16
|
-
"postinstall": "node -e \"require('fs').rmSync('dist', { recursive: true })\""
|
|
23
|
+
"test": "mocha 'lib/**/*.spec.ts' 'stable/**/*.spec.ts' 'draft-*/**/*.spec.ts'"
|
|
17
24
|
},
|
|
18
|
-
"repository": "github:hyperjump-io/json-schema
|
|
25
|
+
"repository": "github:hyperjump-io/json-schema",
|
|
19
26
|
"keywords": [
|
|
20
27
|
"JSON Schema",
|
|
21
|
-
"
|
|
28
|
+
"JSON",
|
|
29
|
+
"Schema",
|
|
30
|
+
"2020-12",
|
|
31
|
+
"2019-09",
|
|
22
32
|
"draft-06",
|
|
23
|
-
"draft-
|
|
24
|
-
"draft 2019-09",
|
|
25
|
-
"draft 2020-12",
|
|
33
|
+
"draft-04",
|
|
26
34
|
"vocabulary",
|
|
27
35
|
"vocabularies"
|
|
28
36
|
],
|
|
@@ -33,8 +41,6 @@
|
|
|
33
41
|
"url": "https://github.com/sponsors/jdesrosiers"
|
|
34
42
|
},
|
|
35
43
|
"devDependencies": {
|
|
36
|
-
"@rollup/plugin-commonjs": "*",
|
|
37
|
-
"@rollup/plugin-node-resolve": "*",
|
|
38
44
|
"@types/chai": "*",
|
|
39
45
|
"@types/mocha": "*",
|
|
40
46
|
"@typescript-eslint/eslint-plugin": "*",
|
|
@@ -46,14 +52,17 @@
|
|
|
46
52
|
"eslint-plugin-import": "*",
|
|
47
53
|
"json-schema-test-suite": "github:json-schema-org/JSON-Schema-Test-Suite",
|
|
48
54
|
"mocha": "*",
|
|
49
|
-
"
|
|
50
|
-
"rollup": "*",
|
|
51
|
-
"rollup-plugin-terser": "*",
|
|
55
|
+
"nock": "*",
|
|
52
56
|
"ts-node": "*",
|
|
53
|
-
"typescript": "*"
|
|
57
|
+
"typescript": "*",
|
|
58
|
+
"yaml": "*"
|
|
54
59
|
},
|
|
55
60
|
"dependencies": {
|
|
56
|
-
"@hyperjump/json
|
|
57
|
-
"
|
|
61
|
+
"@hyperjump/json": "^0.1.0",
|
|
62
|
+
"@hyperjump/json-pointer": "^0.9.5",
|
|
63
|
+
"@hyperjump/pact": "^0.2.4",
|
|
64
|
+
"content-type": "^1.0.4",
|
|
65
|
+
"fastest-stable-stringify": "^2.0.2",
|
|
66
|
+
"node-fetch": "^3.3.0"
|
|
58
67
|
}
|
|
59
68
|
}
|