mirror of
https://github.com/github/codeql-action.git
synced 2026-10-03 17:41:28 +00:00
Merge pull request #4006 from github/mbg/json/schema-checker
Add schema checker to `json` module
This commit is contained in:
128
lib/entry-points.js
generated
128
lib/entry-points.js
generated
@@ -29647,13 +29647,13 @@ var require_helpers = __commonJS({
|
||||
exports2.encodePath = function encodePointer(a) {
|
||||
return a.map(pathEncoder).join("");
|
||||
};
|
||||
exports2.getDecimalPlaces = function getDecimalPlaces(number) {
|
||||
exports2.getDecimalPlaces = function getDecimalPlaces(number2) {
|
||||
var decimalPlaces = 0;
|
||||
if (isNaN(number)) return decimalPlaces;
|
||||
if (typeof number !== "number") {
|
||||
number = Number(number);
|
||||
if (isNaN(number2)) return decimalPlaces;
|
||||
if (typeof number2 !== "number") {
|
||||
number2 = Number(number2);
|
||||
}
|
||||
var parts = number.toString().split("e");
|
||||
var parts = number2.toString().split("e");
|
||||
if (parts.length === 2) {
|
||||
if (parts[1][0] !== "-") {
|
||||
return decimalPlaces;
|
||||
@@ -78733,9 +78733,9 @@ var require_enum_object = __commonJS({
|
||||
if (!isEnumObject(enumObject))
|
||||
throw new Error("not a typescript enum object");
|
||||
let values = [];
|
||||
for (let [name, number] of Object.entries(enumObject))
|
||||
if (typeof number == "number")
|
||||
values.push({ name, number });
|
||||
for (let [name, number2] of Object.entries(enumObject))
|
||||
if (typeof number2 == "number")
|
||||
values.push({ name, number: number2 });
|
||||
return values;
|
||||
}
|
||||
exports2.listEnumValues = listEnumValues;
|
||||
@@ -92751,10 +92751,10 @@ var require_util12 = __commonJS({
|
||||
return arg == null;
|
||||
}
|
||||
exports2.isNullOrUndefined = isNullOrUndefined;
|
||||
function isNumber(arg) {
|
||||
function isNumber2(arg) {
|
||||
return typeof arg === "number";
|
||||
}
|
||||
exports2.isNumber = isNumber;
|
||||
exports2.isNumber = isNumber2;
|
||||
function isString3(arg) {
|
||||
return typeof arg === "string";
|
||||
}
|
||||
@@ -101524,24 +101524,24 @@ var require_operators = __commonJS({
|
||||
}
|
||||
}.call(this);
|
||||
}
|
||||
function toIntegerOrInfinity(number) {
|
||||
number = Number2(number);
|
||||
if (NumberIsNaN(number)) {
|
||||
function toIntegerOrInfinity(number2) {
|
||||
number2 = Number2(number2);
|
||||
if (NumberIsNaN(number2)) {
|
||||
return 0;
|
||||
}
|
||||
if (number < 0) {
|
||||
throw new ERR_OUT_OF_RANGE("number", ">= 0", number);
|
||||
if (number2 < 0) {
|
||||
throw new ERR_OUT_OF_RANGE("number", ">= 0", number2);
|
||||
}
|
||||
return number;
|
||||
return number2;
|
||||
}
|
||||
function drop(number, options = void 0) {
|
||||
function drop(number2, options = void 0) {
|
||||
if (options != null) {
|
||||
validateObject(options, "options");
|
||||
}
|
||||
if ((options === null || options === void 0 ? void 0 : options.signal) != null) {
|
||||
validateAbortSignal(options.signal, "options.signal");
|
||||
}
|
||||
number = toIntegerOrInfinity(number);
|
||||
number2 = toIntegerOrInfinity(number2);
|
||||
return async function* drop2() {
|
||||
var _options$signal5;
|
||||
if (options !== null && options !== void 0 && (_options$signal5 = options.signal) !== null && _options$signal5 !== void 0 && _options$signal5.aborted) {
|
||||
@@ -101552,20 +101552,20 @@ var require_operators = __commonJS({
|
||||
if (options !== null && options !== void 0 && (_options$signal6 = options.signal) !== null && _options$signal6 !== void 0 && _options$signal6.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
if (number-- <= 0) {
|
||||
if (number2-- <= 0) {
|
||||
yield val;
|
||||
}
|
||||
}
|
||||
}.call(this);
|
||||
}
|
||||
function take(number, options = void 0) {
|
||||
function take(number2, options = void 0) {
|
||||
if (options != null) {
|
||||
validateObject(options, "options");
|
||||
}
|
||||
if ((options === null || options === void 0 ? void 0 : options.signal) != null) {
|
||||
validateAbortSignal(options.signal, "options.signal");
|
||||
}
|
||||
number = toIntegerOrInfinity(number);
|
||||
number2 = toIntegerOrInfinity(number2);
|
||||
return async function* take2() {
|
||||
var _options$signal7;
|
||||
if (options !== null && options !== void 0 && (_options$signal7 = options.signal) !== null && _options$signal7 !== void 0 && _options$signal7.aborted) {
|
||||
@@ -101576,10 +101576,10 @@ var require_operators = __commonJS({
|
||||
if (options !== null && options !== void 0 && (_options$signal8 = options.signal) !== null && _options$signal8 !== void 0 && _options$signal8.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
if (number-- > 0) {
|
||||
if (number2-- > 0) {
|
||||
yield val;
|
||||
}
|
||||
if (number <= 0) {
|
||||
if (number2 <= 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -124893,8 +124893,8 @@ var require_util16 = __commonJS({
|
||||
parts.push(format.substring(last));
|
||||
return parts.join("");
|
||||
};
|
||||
util3.formatNumber = function(number, decimals, dec_point, thousands_sep) {
|
||||
var n = number, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals;
|
||||
util3.formatNumber = function(number2, decimals, dec_point, thousands_sep) {
|
||||
var n = number2, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals;
|
||||
var d = dec_point === void 0 ? "," : dec_point;
|
||||
var t = thousands_sep === void 0 ? "." : thousands_sep, s = n < 0 ? "-" : "";
|
||||
var i = parseInt(n = Math.abs(+n || 0).toFixed(c), 10) + "";
|
||||
@@ -144502,35 +144502,95 @@ function isArray(value) {
|
||||
function isString(value) {
|
||||
return typeof value === "string";
|
||||
}
|
||||
function isNumber(value) {
|
||||
return typeof value === "number";
|
||||
}
|
||||
function isStringOrUndefined(value) {
|
||||
return value === void 0 || isString(value);
|
||||
}
|
||||
var string = {
|
||||
validate: isString,
|
||||
required: true
|
||||
};
|
||||
function defaultCheck(validate) {
|
||||
return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) });
|
||||
}
|
||||
function makeValidator(validate, required = true) {
|
||||
return {
|
||||
validate,
|
||||
check: defaultCheck(validate),
|
||||
required
|
||||
};
|
||||
}
|
||||
var string = makeValidator(isString);
|
||||
var number = makeValidator(isNumber);
|
||||
function optionalOrNull(validator) {
|
||||
return {
|
||||
validate: (val) => {
|
||||
return val === void 0 || val === null || validator.validate(val);
|
||||
},
|
||||
check: (val, opts, path29) => {
|
||||
if (val === void 0 || val === null) {
|
||||
return successfulCheckSchema();
|
||||
}
|
||||
return validator.check(val, opts, path29);
|
||||
},
|
||||
required: false
|
||||
};
|
||||
}
|
||||
function validateSchema(schema, obj) {
|
||||
const result = checkSchema(schema, obj, { failFast: true });
|
||||
return result.valid;
|
||||
}
|
||||
function successfulCheckSchema() {
|
||||
return {
|
||||
valid: true,
|
||||
unknownKeys: [],
|
||||
invalidKeys: []
|
||||
};
|
||||
}
|
||||
function checkSchema(schema, obj, options = {}, path29 = "") {
|
||||
const result = successfulCheckSchema();
|
||||
const inputKeys = new Set(Object.keys(obj));
|
||||
const invalidKeys = /* @__PURE__ */ new Set();
|
||||
for (const [key, validator] of Object.entries(schema)) {
|
||||
const hasKey = key in obj;
|
||||
inputKeys.delete(key);
|
||||
invalidKeys.add(key);
|
||||
if (validator.required && !hasKey) {
|
||||
return false;
|
||||
result.valid = false;
|
||||
if (options.failFast) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (validator.required && (obj[key] === void 0 || obj[key] === null)) {
|
||||
return false;
|
||||
result.valid = false;
|
||||
if (options.failFast) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (hasKey && !validator.validate(obj[key])) {
|
||||
return false;
|
||||
if (hasKey) {
|
||||
const checkResult = validator.check(obj[key], options, `${path29}.${key}`);
|
||||
result.unknownKeys.push(...checkResult.unknownKeys);
|
||||
result.invalidKeys.push(...checkResult.invalidKeys);
|
||||
if (checkResult.invalidKeys.length > 0) {
|
||||
invalidKeys.delete(key);
|
||||
}
|
||||
if (!checkResult.valid) {
|
||||
result.valid = false;
|
||||
if (options.failFast) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
invalidKeys.delete(key);
|
||||
}
|
||||
return true;
|
||||
for (const remainingKey of inputKeys) {
|
||||
result.unknownKeys.push(`${path29}.${remainingKey}`);
|
||||
}
|
||||
for (const invalidKey of invalidKeys) {
|
||||
result.invalidKeys.push(`${path29}.${invalidKey}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// src/util.ts
|
||||
|
||||
@@ -67,3 +67,76 @@ test("validateSchema - optional properties are optional", async (t) => {
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: "" }));
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: "foo" }));
|
||||
});
|
||||
|
||||
const arraySchema = {
|
||||
arrayKey: json.array(json.number),
|
||||
};
|
||||
|
||||
test("validateSchema - validates arrays", async (t) => {
|
||||
// Arrays of numeric elements are accepted.
|
||||
t.true(json.validateSchema(arraySchema, { arrayKey: [] }));
|
||||
t.true(json.validateSchema(arraySchema, { arrayKey: [4] }));
|
||||
t.true(json.validateSchema(arraySchema, { arrayKey: [4, 8] }));
|
||||
t.true(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15] }));
|
||||
|
||||
// Other array elements are not accepted.
|
||||
t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15, "bar"] }));
|
||||
t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, undefined] }));
|
||||
t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15, null] }));
|
||||
});
|
||||
|
||||
const objectSchema = {
|
||||
objectKey: json.object(arraySchema),
|
||||
};
|
||||
|
||||
test("validateSchema - validates objects", async (t) => {
|
||||
// Objects of the given schema are accepted.
|
||||
t.true(json.validateSchema(objectSchema, { objectKey: { arrayKey: [] } }));
|
||||
t.true(json.validateSchema(objectSchema, { objectKey: { arrayKey: [4] } }));
|
||||
|
||||
// Other values are not accepted.
|
||||
t.false(json.validateSchema(objectSchema, {}));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: [] }));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: undefined }));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: null }));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: "foo" }));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: 123 }));
|
||||
});
|
||||
|
||||
const checkSchemaTestSchema = {
|
||||
rootKey: json.object(objectSchema),
|
||||
};
|
||||
|
||||
test("checkSchema - reports unknown keys", async (t) => {
|
||||
const result = json.checkSchema(checkSchemaTestSchema, {
|
||||
rootKey: {
|
||||
objectKey: {
|
||||
arrayKey: [],
|
||||
},
|
||||
nestedExtraKey: "foo",
|
||||
},
|
||||
extraKey: "bar",
|
||||
});
|
||||
|
||||
t.true(result.valid);
|
||||
t.deepEqual(
|
||||
result.unknownKeys.sort(),
|
||||
[".extraKey", ".rootKey.nestedExtraKey"].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
test("checkSchema - reports invalid keys", async (t) => {
|
||||
const result = json.checkSchema(checkSchemaTestSchema, {
|
||||
rootKey: {
|
||||
objectKey: {
|
||||
arrayKey: ["foo"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
t.false(result.valid);
|
||||
t.deepEqual(
|
||||
result.invalidKeys.sort(),
|
||||
[".rootKey.objectKey.arrayKey[0]"].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -48,23 +48,107 @@ export function isStringOrUndefined(
|
||||
*/
|
||||
export type Validator<T> = {
|
||||
validate: (val: unknown) => val is T;
|
||||
check: (
|
||||
val: unknown,
|
||||
opts: CheckSchemaOptions,
|
||||
path: string,
|
||||
) => CheckSchemaResult;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
function defaultCheck(
|
||||
validate: (val: unknown) => val is any,
|
||||
): (arg: unknown) => CheckSchemaResult {
|
||||
return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) });
|
||||
}
|
||||
|
||||
function makeValidator<T>(
|
||||
validate: (arg: unknown) => arg is T,
|
||||
required: boolean = true,
|
||||
) {
|
||||
return {
|
||||
validate,
|
||||
check: defaultCheck(validate),
|
||||
required,
|
||||
} as const satisfies Validator<T>;
|
||||
}
|
||||
|
||||
/** Extracts `T` from `Validator<T>`. */
|
||||
export type UnwrapValidator<V> = V extends Validator<infer A> ? A : never;
|
||||
|
||||
/** A validator for string fields in schemas. */
|
||||
export const string = {
|
||||
validate: isString,
|
||||
required: true,
|
||||
} as const satisfies Validator<string>;
|
||||
export const string = makeValidator(isString);
|
||||
|
||||
/** A validator for number fields in schemas. */
|
||||
export const number = {
|
||||
validate: isNumber,
|
||||
required: true,
|
||||
} as const satisfies Validator<number>;
|
||||
export const number = makeValidator(isNumber);
|
||||
|
||||
/** A validator for arrays. */
|
||||
export function array<T>(validator: Validator<T>) {
|
||||
const validate = (val: unknown) => {
|
||||
return isArray(val) && val.every((e) => validator.validate(e));
|
||||
};
|
||||
return {
|
||||
validate,
|
||||
check: (val: unknown, opts: CheckSchemaOptions, path: string) => {
|
||||
const result: CheckSchemaResult = successfulCheckSchema();
|
||||
|
||||
// The value must be an array.
|
||||
if (!isArray(val)) {
|
||||
result.valid = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Validate all elements of the array.
|
||||
let index = 0;
|
||||
for (const e of val) {
|
||||
const elementPath = `${path}[${index}]`;
|
||||
const eResult = validator.check(e, opts, `${elementPath}`);
|
||||
|
||||
result.invalidKeys.push(...eResult.invalidKeys);
|
||||
result.unknownKeys.push(...eResult.unknownKeys);
|
||||
index++;
|
||||
|
||||
if (!eResult.valid) {
|
||||
result.valid = false;
|
||||
|
||||
// Add the element path to `invalidKeys` if we didn't get
|
||||
// any more specific ones from the element validator.
|
||||
if (eResult.invalidKeys.length === 0) {
|
||||
result.invalidKeys.push(elementPath);
|
||||
}
|
||||
|
||||
if (opts.failFast) {
|
||||
return result;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
required: true,
|
||||
} as const satisfies Validator<T[]>;
|
||||
}
|
||||
|
||||
/** A validator for objects. */
|
||||
export function object<
|
||||
S extends Schema,
|
||||
T extends UnvalidatedObject<any> = FromSchema<S>,
|
||||
>(schema: S) {
|
||||
return {
|
||||
validate: (val: unknown) => {
|
||||
return isObject(val) && validateSchema<S, T>(schema, val);
|
||||
},
|
||||
check: (val, opts, path) => {
|
||||
if (!isObject(val)) {
|
||||
return invalidCheckSchema();
|
||||
}
|
||||
return checkSchema(schema, val, opts, path);
|
||||
},
|
||||
required: true,
|
||||
} as const satisfies Validator<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a validator to be optional, accepting `undefined` or `null` for an
|
||||
@@ -75,6 +159,12 @@ export function optionalOrNull<T>(validator: Validator<T>) {
|
||||
validate: (val: unknown) => {
|
||||
return val === undefined || val === null || validator.validate(val);
|
||||
},
|
||||
check: (val, opts, path) => {
|
||||
if (val === undefined || val === null) {
|
||||
return successfulCheckSchema();
|
||||
}
|
||||
return validator.check(val, opts, path);
|
||||
},
|
||||
required: false,
|
||||
} as const satisfies Validator<T | undefined | null>;
|
||||
}
|
||||
@@ -88,6 +178,12 @@ export function optional<T>(validator: Validator<T>) {
|
||||
validate: (val: unknown): val is T | undefined => {
|
||||
return val === undefined || validator.validate(val);
|
||||
},
|
||||
check: (val, opts, path) => {
|
||||
if (val === undefined) {
|
||||
return successfulCheckSchema();
|
||||
}
|
||||
return validator.check(val, opts, path);
|
||||
},
|
||||
required: false,
|
||||
} as const satisfies Validator<T | undefined>;
|
||||
}
|
||||
@@ -117,28 +213,133 @@ export type FromSchema<S extends Schema> = {
|
||||
* @param obj The object to validate.
|
||||
* @returns Asserts that `obj` is of the `schema`'s type if validation is successful.
|
||||
*/
|
||||
export function validateSchema<S extends Schema>(
|
||||
export function validateSchema<
|
||||
S extends Schema,
|
||||
T extends UnvalidatedObject<any> = FromSchema<S>,
|
||||
>(schema: S, obj: UnvalidatedObject<any>): obj is T {
|
||||
const result = checkSchema(schema, obj, { failFast: true });
|
||||
return result.valid;
|
||||
}
|
||||
|
||||
export interface CheckSchemaOptions {
|
||||
/** Whether to stop validation after the first error. */
|
||||
failFast?: boolean;
|
||||
}
|
||||
|
||||
export interface CheckSchemaResult {
|
||||
/** Whether the `obj` satisfies the schema. */
|
||||
valid: boolean;
|
||||
/** Unknown keys that were found during validation. */
|
||||
unknownKeys: string[];
|
||||
/** Known keys that failed validation. */
|
||||
invalidKeys: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to produce a `CheckSchemaResult` where `valid: true`.
|
||||
*/
|
||||
function successfulCheckSchema(): CheckSchemaResult {
|
||||
return {
|
||||
valid: true,
|
||||
unknownKeys: [],
|
||||
invalidKeys: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to produce a `CheckSchemaResult` where `valid: false`.
|
||||
*/
|
||||
function invalidCheckSchema(): CheckSchemaResult {
|
||||
return {
|
||||
valid: false,
|
||||
unknownKeys: [],
|
||||
invalidKeys: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function checkSchema<S extends Schema>(
|
||||
schema: S,
|
||||
obj: UnvalidatedObject<any>,
|
||||
): obj is FromSchema<S> {
|
||||
options: CheckSchemaOptions = {},
|
||||
path: string = "",
|
||||
): CheckSchemaResult {
|
||||
const result: CheckSchemaResult = successfulCheckSchema();
|
||||
|
||||
// Track the set of input keys. We remove keys from this set as we recognise them
|
||||
// during validation.
|
||||
const inputKeys = new Set(Object.keys(obj));
|
||||
|
||||
// Track keys that have failed validation, starting with the empty set.
|
||||
const invalidKeys = new Set();
|
||||
|
||||
// Loop through all keys in the object schema and validate that the given object
|
||||
// satisfies the schema key.
|
||||
for (const [key, validator] of Object.entries(schema)) {
|
||||
const hasKey = key in obj;
|
||||
|
||||
// Remove key from set of unrecognised keys.
|
||||
inputKeys.delete(key);
|
||||
|
||||
// Add the key to the set of invalid keys. We remove it later once
|
||||
// it passes validation.
|
||||
invalidKeys.add(key);
|
||||
|
||||
// If the property is required, but absent, fail.
|
||||
if (validator.required && !hasKey) {
|
||||
return false;
|
||||
result.valid = false;
|
||||
|
||||
if (options.failFast) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the property is required, but undefined or null, fail.
|
||||
if (validator.required && (obj[key] === undefined || obj[key] === null)) {
|
||||
return false;
|
||||
result.valid = false;
|
||||
|
||||
if (options.failFast) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the property is present, validate it.
|
||||
if (hasKey && !validator.validate(obj[key])) {
|
||||
return false;
|
||||
if (hasKey) {
|
||||
const checkResult = validator.check(obj[key], options, `${path}.${key}`);
|
||||
|
||||
result.unknownKeys.push(...checkResult.unknownKeys);
|
||||
result.invalidKeys.push(...checkResult.invalidKeys);
|
||||
|
||||
// If we have invalid keys from the validator, then that means that
|
||||
// we have a more specific key than `key`. Remove `key` from the results.
|
||||
if (checkResult.invalidKeys.length > 0) {
|
||||
invalidKeys.delete(key);
|
||||
}
|
||||
|
||||
if (!checkResult.valid) {
|
||||
result.valid = false;
|
||||
|
||||
if (options.failFast) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach this point, the key has been successfully validated.
|
||||
invalidKeys.delete(key);
|
||||
}
|
||||
|
||||
return true;
|
||||
// If there are any remaining keys in `inputKeys`, add them to `unknownKeys`.
|
||||
for (const remainingKey of inputKeys) {
|
||||
result.unknownKeys.push(`${path}.${remainingKey}`);
|
||||
}
|
||||
|
||||
// If there are any remaining keys in `invalidKeys`, add them to the result.
|
||||
for (const invalidKey of invalidKeys) {
|
||||
result.invalidKeys.push(`${path}.${invalidKey}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user