Son CV dans un terminal web en Javascript! https://terminal-cv.gregandev.fr
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

506 lines
15 KiB

// @flow strict-local
import ThrowableDiagnostic, {
generateJSONCodeHighlights,
escapeMarkdown,
encodeJSONKeyComponent,
} from '@parcel/diagnostic';
import type {Mapping} from 'json-source-map';
import nullthrows from 'nullthrows';
// flowlint-next-line untyped-import:off
import levenshtein from 'fastest-levenshtein';
export type SchemaEntity =
| SchemaObject
| SchemaArray
| SchemaBoolean
| SchemaString
| SchemaNumber
| SchemaEnum
| SchemaOneOf
| SchemaAllOf
| SchemaNot
| SchemaAny;
export type SchemaArray = {|
type: 'array',
items?: SchemaEntity,
__type?: string,
|};
export type SchemaBoolean = {|
type: 'boolean',
__type?: string,
|};
export type SchemaOneOf = {|
oneOf: Array<SchemaEntity>,
|};
export type SchemaAllOf = {|
allOf: Array<SchemaEntity>,
|};
export type SchemaNot = {|
not: SchemaEntity,
__message: string,
|};
export type SchemaString = {|
type: 'string',
enum?: Array<string>,
__validate?: (val: string) => ?string,
__type?: string,
|};
export type SchemaNumber = {|
type: 'number',
enum?: Array<number>,
__type?: string,
|};
export type SchemaEnum = {|
enum: Array<mixed>,
|};
export type SchemaObject = {|
type: 'object',
properties: {[string]: SchemaEntity, ...},
additionalProperties?: boolean | SchemaEntity,
required?: Array<string>,
__forbiddenProperties?: Array<string>,
__type?: string,
|};
export type SchemaAny = {||};
export type SchemaError =
| {|
type: 'type',
expectedTypes: Array<string>,
dataType: ?'key' | 'value',
dataPath: string,
ancestors: Array<SchemaEntity>,
prettyType?: string,
|}
| {|
type: 'enum',
expectedValues: Array<mixed>,
dataType: 'key' | 'value',
actualValue: mixed,
dataPath: string,
ancestors: Array<SchemaEntity>,
prettyType?: string,
|}
| {|
type: 'forbidden-prop',
prop: string,
expectedProps: Array<string>,
actualProps: Array<string>,
dataType: 'key',
dataPath: string,
ancestors: Array<SchemaEntity>,
prettyType?: string,
|}
| {|
type: 'missing-prop',
prop: string,
expectedProps: Array<string>,
actualProps: Array<string>,
dataType: 'key' | 'value',
dataPath: string,
ancestors: Array<SchemaEntity>,
prettyType?: string,
|}
| {|
type: 'other',
actualValue: mixed,
dataType: ?'key' | 'value',
message?: string,
dataPath: string,
ancestors: Array<SchemaEntity>,
|};
function validateSchema(schema: SchemaEntity, data: mixed): Array<SchemaError> {
function walk(
schemaAncestors,
dataNode,
dataPath,
): ?SchemaError | Array<SchemaError> {
let [schemaNode] = schemaAncestors;
if (schemaNode.type) {
let type = Array.isArray(dataNode) ? 'array' : typeof dataNode;
if (schemaNode.type !== type) {
return {
type: 'type',
dataType: 'value',
dataPath,
expectedTypes: [schemaNode.type],
ancestors: schemaAncestors,
prettyType: schemaNode.__type,
};
} else {
switch (schemaNode.type) {
case 'array': {
if (schemaNode.items) {
let results: Array<SchemaError | Array<SchemaError>> = [];
// $FlowFixMe type was already checked
for (let i = 0; i < dataNode.length; i++) {
let result = walk(
[schemaNode.items].concat(schemaAncestors),
// $FlowFixMe type was already checked
dataNode[i],
dataPath + '/' + i,
);
if (result) results.push(result);
}
if (results.length)
return results.reduce((acc, v) => acc.concat(v), []);
}
break;
}
case 'string': {
// $FlowFixMe type was already checked
let value: string = dataNode;
if (schemaNode.enum) {
if (!schemaNode.enum.includes(value)) {
return {
type: 'enum',
dataType: 'value',
dataPath,
expectedValues: schemaNode.enum,
actualValue: value,
ancestors: schemaAncestors,
};
}
} else if (schemaNode.__validate) {
let validationError = schemaNode.__validate(value);
if (typeof validationError == 'string') {
return {
type: 'other',
dataType: 'value',
dataPath,
message: validationError,
actualValue: value,
ancestors: schemaAncestors,
};
}
}
break;
}
case 'number': {
// $FlowFixMe type was already checked
let value: number = dataNode;
if (schemaNode.enum) {
if (!schemaNode.enum.includes(value)) {
return {
type: 'enum',
dataType: 'value',
dataPath,
expectedValues: schemaNode.enum,
actualValue: value,
ancestors: schemaAncestors,
};
}
}
break;
}
case 'object': {
let results: Array<Array<SchemaError> | SchemaError> = [];
let invalidProps;
if (schemaNode.__forbiddenProperties) {
// $FlowFixMe type was already checked
let keys = Object.keys(dataNode);
invalidProps = schemaNode.__forbiddenProperties.filter(val =>
keys.includes(val),
);
results.push(
...invalidProps.map(
k =>
({
type: 'forbidden-prop',
dataPath: dataPath + '/' + encodeJSONKeyComponent(k),
dataType: 'key',
prop: k,
expectedProps: Object.keys(schemaNode.properties),
actualProps: keys,
ancestors: schemaAncestors,
}: SchemaError),
),
);
}
if (schemaNode.required) {
// $FlowFixMe type was already checked
let keys = Object.keys(dataNode);
let missingKeys = schemaNode.required.filter(
val => !keys.includes(val),
);
results.push(
...missingKeys.map(
k =>
({
type: 'missing-prop',
dataPath,
dataType: 'value',
prop: k,
expectedProps: schemaNode.required,
actualProps: keys,
ancestors: schemaAncestors,
}: SchemaError),
),
);
}
if (schemaNode.properties) {
let {additionalProperties = true} = schemaNode;
// $FlowFixMe type was already checked
for (let k in dataNode) {
if (invalidProps && invalidProps.includes(k)) {
// Don't check type on forbidden props
continue;
} else if (k in schemaNode.properties) {
let result = walk(
[schemaNode.properties[k]].concat(schemaAncestors),
// $FlowFixMe type was already checked
dataNode[k],
dataPath + '/' + encodeJSONKeyComponent(k),
);
if (result) results.push(result);
} else {
if (typeof additionalProperties === 'boolean') {
if (!additionalProperties) {
results.push({
type: 'enum',
dataType: 'key',
dataPath: dataPath + '/' + encodeJSONKeyComponent(k),
expectedValues: Object.keys(
schemaNode.properties,
).filter(
// $FlowFixMe type was already checked
p => !(p in dataNode),
),
actualValue: k,
ancestors: schemaAncestors,
prettyType: schemaNode.__type,
});
}
} else {
let result = walk(
[additionalProperties].concat(schemaAncestors),
// $FlowFixMe type was already checked
dataNode[k],
dataPath + '/' + encodeJSONKeyComponent(k),
);
if (result) results.push(result);
}
}
}
}
if (results.length)
return results.reduce((acc, v) => acc.concat(v), []);
break;
}
case 'boolean':
// NOOP, type was checked already
break;
default:
throw new Error(`Unimplemented schema type ${type}?`);
}
}
} else {
if (schemaNode.enum && !schemaNode.enum.includes(dataNode)) {
return {
type: 'enum',
dataType: 'value',
dataPath: dataPath,
expectedValues: schemaNode.enum,
actualValue: schemaNode,
ancestors: schemaAncestors,
};
}
if (schemaNode.oneOf || schemaNode.allOf) {
let list = schemaNode.oneOf || schemaNode.allOf;
let results: Array<SchemaError | Array<SchemaError>> = [];
for (let f of list) {
let result = walk([f].concat(schemaAncestors), dataNode, dataPath);
if (result) results.push(result);
}
if (
schemaNode.oneOf
? results.length == schemaNode.oneOf.length
: results.length > 0
) {
// return the result with more values / longer key
results.sort((a, b) =>
Array.isArray(a) || Array.isArray(b)
? Array.isArray(a) && !Array.isArray(b)
? -1
: !Array.isArray(a) && Array.isArray(b)
? 1
: Array.isArray(a) && Array.isArray(b)
? b.length - a.length
: 0
: b.dataPath.length - a.dataPath.length,
);
return results[0];
}
} else if (schemaNode.not) {
let result = walk(
[schemaNode.not].concat(schemaAncestors),
dataNode,
dataPath,
);
if (!result || result.length == 0) {
return {
type: 'other',
dataPath,
dataType: null,
message: schemaNode.__message,
actualValue: dataNode,
ancestors: schemaAncestors,
};
}
}
}
return undefined;
}
let result = walk([schema], data, '');
return Array.isArray(result) ? result : result ? [result] : [];
}
export default validateSchema;
export function fuzzySearch(
expectedValues: Array<string>,
actualValue: string,
): Array<string> {
let result = expectedValues
.map(exp => [exp, levenshtein.distance(exp, actualValue)])
.filter(
// Remove if more than half of the string would need to be changed
([, d]) => d * 2 < actualValue.length,
);
result.sort(([, a], [, b]) => a - b);
return result.map(([v]) => v);
}
validateSchema.diagnostic = function (
schema: SchemaEntity,
data: {|
...
| {|
source?: ?string,
data?: mixed,
|}
| {|
source: string,
map: {|
data: mixed,
pointers: {|[key: string]: Mapping|},
|},
|},
filePath?: ?string,
prependKey?: ?string,
|},
origin: string,
message: string,
): void {
if (
'source' in data &&
'data' in data &&
typeof data.source !== 'string' &&
!data
) {
throw new Error(
'At least one of data.source and data.source must be defined!',
);
}
let object = data.map
? data.map.data
: // $FlowFixMe we can assume it's a JSON object
data.data ?? JSON.parse(data.source);
let errors = validateSchema(schema, object);
if (errors.length) {
let keys = errors.map(e => {
let message;
if (e.type === 'enum') {
let {actualValue} = e;
let expectedValues = e.expectedValues.map(String);
let likely =
actualValue != null
? fuzzySearch(expectedValues, String(actualValue))
: [];
if (likely.length > 0) {
message = `Did you mean ${likely
.map(v => JSON.stringify(v))
.join(', ')}?`;
} else if (expectedValues.length > 0) {
message = `Possible values: ${expectedValues
.map(v => JSON.stringify(v))
.join(', ')}`;
} else {
message = 'Unexpected value';
}
} else if (e.type === 'forbidden-prop') {
let {prop, expectedProps, actualProps} = e;
let likely = fuzzySearch(expectedProps, prop).filter(
v => !actualProps.includes(v),
);
if (likely.length > 0) {
message = `Did you mean ${likely
.map(v => JSON.stringify(v))
.join(', ')}?`;
} else {
message = 'Unexpected property';
}
} else if (e.type === 'missing-prop') {
let {prop, actualProps} = e;
let likely = fuzzySearch(actualProps, prop);
if (likely.length > 0) {
message = `Did you mean ${JSON.stringify(prop)}?`;
e.dataPath += '/' + likely[0];
e.dataType = 'key';
} else {
message = `Missing property ${prop}`;
}
} else if (e.type === 'type') {
if (e.prettyType != null) {
message = `Expected ${e.prettyType}`;
} else {
message = `Expected type ${e.expectedTypes.join(', ')}`;
}
} else {
message = e.message;
}
return {key: e.dataPath, type: e.dataType, message};
});
let map, code;
if (data.map) {
map = data.map;
code = data.source;
} else {
// $FlowFixMe we can assume that data is valid JSON
map = data.source ?? JSON.stringify(nullthrows(data.data), 0, '\t');
code = map;
}
let codeFrames = [
{
filePath: data.filePath ?? undefined,
language: 'json',
code,
codeHighlights: generateJSONCodeHighlights(
map,
keys.map(({key, type, message}) => ({
key: (data.prependKey ?? '') + key,
type: type,
message: message != null ? escapeMarkdown(message) : message,
})),
),
},
];
throw new ThrowableDiagnostic({
diagnostic: {
message: message,
origin,
codeFrames,
},
});
}
};