Files
Elydora-Node-SDK/src/plugins/strict-json.ts
T

75 lines
2.1 KiB
TypeScript

import {
parse,
parseTree,
printParseErrorCode,
type Node,
type ParseError,
} from 'jsonc-parser';
export type JsonObject = Record<string, unknown>;
interface JsonObjectParseOptions {
readonly allowTrailingComma: boolean;
readonly disallowComments: boolean;
}
export function isObject(value: unknown): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function rejectDuplicateKeys(
node: Node | undefined,
label: string,
location: string[] = [],
): void {
if (!node) return;
if (node.type === 'object') {
const keys = new Set<string>();
for (const property of node.children ?? []) {
const key = String(property.children?.[0]?.value);
const childLocation = [...location, key];
if (keys.has(key)) {
throw new Error(`${label} contains duplicate field "${childLocation.join('.')}"`);
}
keys.add(key);
rejectDuplicateKeys(property.children?.[1], label, childLocation);
}
return;
}
if (node.type === 'array') {
for (const child of node.children ?? []) rejectDuplicateKeys(child, label, location);
}
}
function parseJsonObject(
raw: string,
label: string,
options: JsonObjectParseOptions,
): JsonObject {
const errors: ParseError[] = [];
const value: unknown = parse(raw, errors, options);
if (errors.length > 0) {
const details = errors
.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`)
.join(', ');
throw new Error(`Failed to parse ${label}: ${details}`);
}
if (!isObject(value)) throw new Error(`${label} must contain a JSON object`);
rejectDuplicateKeys(parseTree(raw, [], options), label);
return value;
}
export function parseStrictJsonObject(raw: string, label: string): JsonObject {
return parseJsonObject(raw, label, {
allowTrailingComma: false,
disallowComments: true,
});
}
export function parseStrictJsoncObject(raw: string, label: string): JsonObject {
return parseJsonObject(raw, label, {
allowTrailingComma: true,
disallowComments: false,
});
}