summaryrefslogtreecommitdiff
path: root/examples/server/public/json-schema-to-grammar.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'examples/server/public/json-schema-to-grammar.mjs')
-rw-r--r--examples/server/public/json-schema-to-grammar.mjs835
1 files changed, 0 insertions, 835 deletions
diff --git a/examples/server/public/json-schema-to-grammar.mjs b/examples/server/public/json-schema-to-grammar.mjs
deleted file mode 100644
index 7267f3f9..00000000
--- a/examples/server/public/json-schema-to-grammar.mjs
+++ /dev/null
@@ -1,835 +0,0 @@
-// WARNING: This file was ported from json_schema_to_grammar.py, please fix bugs / add features there first.
-const SPACE_RULE = '| " " | "\\n" [ \\t]{0,20}';
-
-function _buildRepetition(itemRule, minItems, maxItems, opts={}) {
- if (minItems === 0 && maxItems === 1) {
- return `${itemRule}?`;
- }
-
-
- const separatorRule = opts.separatorRule ?? '';
- const itemRuleIsLiteral = opts.itemRuleIsLiteral ?? false
-
- if (separatorRule === '') {
- if (minItems === 1 && maxItems === undefined) {
- return `${itemRule}+`;
- } else if (minItems === 0 && maxItems === undefined) {
- return `${itemRule}*`;
- } else {
- return `${itemRule}{${minItems},${maxItems !== undefined ? maxItems : ''}}`;
- }
- }
-
- const result = itemRule + ' ' + _buildRepetition(`(${separatorRule} ${itemRule})`, minItems > 0 ? minItems - 1 : 0, maxItems !== undefined ? maxItems - 1 : undefined);
- return minItems === 0 ? `(${result})?` : result;
-}
-
-function _generateMinMaxInt(minValue, maxValue, out, decimalsLeft = 16, topLevel = true) {
- const hasMin = minValue !== null;
- const hasMax = maxValue !== null;
-
- function digitRange(fromChar, toChar) {
- out.push("[");
- if (fromChar === toChar) {
- out.push(fromChar);
- } else {
- out.push(fromChar);
- out.push("-");
- out.push(toChar);
- }
- out.push("]");
- }
-
- function moreDigits(minDigits, maxDigits) {
- out.push("[0-9]");
- if (minDigits === maxDigits && minDigits === 1) {
- return;
- }
- out.push("{");
- out.push(minDigits.toString());
- if (maxDigits !== minDigits) {
- out.push(",");
- if (maxDigits !== Number.MAX_SAFE_INTEGER) {
- out.push(maxDigits.toString());
- }
- }
- out.push("}");
- }
-
- function uniformRange(fromStr, toStr) {
- let i = 0;
- while (i < fromStr.length && fromStr[i] === toStr[i]) {
- i++;
- }
- if (i > 0) {
- out.push("\"");
- out.push(fromStr.slice(0, i));
- out.push("\"");
- }
- if (i < fromStr.length) {
- if (i > 0) {
- out.push(" ");
- }
- const subLen = fromStr.length - i - 1;
- if (subLen > 0) {
- const fromSub = fromStr.slice(i + 1);
- const toSub = toStr.slice(i + 1);
- const subZeros = "0".repeat(subLen);
- const subNines = "9".repeat(subLen);
-
- let toReached = false;
- out.push("(");
- if (fromSub === subZeros) {
- digitRange(fromStr[i], String.fromCharCode(toStr.charCodeAt(i) - 1));
- out.push(" ");
- moreDigits(subLen, subLen);
- } else {
- out.push("[");
- out.push(fromStr[i]);
- out.push("] ");
- out.push("(");
- uniformRange(fromSub, subNines);
- out.push(")");
- if (fromStr.charCodeAt(i) < toStr.charCodeAt(i) - 1) {
- out.push(" | ");
- if (toSub === subNines) {
- digitRange(String.fromCharCode(fromStr.charCodeAt(i) + 1), toStr[i]);
- toReached = true;
- } else {
- digitRange(String.fromCharCode(fromStr.charCodeAt(i) + 1), String.fromCharCode(toStr.charCodeAt(i) - 1));
- }
- out.push(" ");
- moreDigits(subLen, subLen);
- }
- }
- if (!toReached) {
- out.push(" | ");
- digitRange(toStr[i], toStr[i]);
- out.push(" ");
- uniformRange(subZeros, toSub);
- }
- out.push(")");
- } else {
- out.push("[");
- out.push(fromStr[i]);
- out.push("-");
- out.push(toStr[i]);
- out.push("]");
- }
- }
- }
-
- if (hasMin && hasMax) {
- if (minValue < 0 && maxValue < 0) {
- out.push("\"-\" (");
- _generateMinMaxInt(-maxValue, -minValue, out, decimalsLeft, true);
- out.push(")");
- return;
- }
-
- if (minValue < 0) {
- out.push("\"-\" (");
- _generateMinMaxInt(0, -minValue, out, decimalsLeft, true);
- out.push(") | ");
- minValue = 0;
- }
-
- let minS = minValue.toString();
- const maxS = maxValue.toString();
- const minDigits = minS.length;
- const maxDigits = maxS.length;
-
- for (let digits = minDigits; digits < maxDigits; digits++) {
- uniformRange(minS, "9".repeat(digits));
- minS = "1" + "0".repeat(digits);
- out.push(" | ");
- }
- uniformRange(minS, maxS);
- return;
- }
-
- const lessDecimals = Math.max(decimalsLeft - 1, 1);
-
- if (hasMin) {
- if (minValue < 0) {
- out.push("\"-\" (");
- _generateMinMaxInt(null, -minValue, out, decimalsLeft, false);
- out.push(") | [0] | [1-9] ");
- moreDigits(0, decimalsLeft - 1);
- } else if (minValue === 0) {
- if (topLevel) {
- out.push("[0] | [1-9] ");
- moreDigits(0, lessDecimals);
- } else {
- moreDigits(1, decimalsLeft);
- }
- } else if (minValue <= 9) {
- const c = minValue.toString();
- const range_start = topLevel ? '1' : '0';
- if (c > range_start) {
- digitRange(range_start, String.fromCharCode(c.charCodeAt(0) - 1));
- out.push(" ");
- moreDigits(1, lessDecimals);
- out.push(" | ");
- }
- digitRange(c, "9");
- out.push(" ");
- moreDigits(0, lessDecimals);
- } else {
- const minS = minValue.toString();
- const length = minS.length;
- const c = minS[0];
-
- if (c > "1") {
- digitRange(topLevel ? "1" : "0", String.fromCharCode(c.charCodeAt(0) - 1));
- out.push(" ");
- moreDigits(length, lessDecimals);
- out.push(" | ");
- }
- digitRange(c, c);
- out.push(" (");
- _generateMinMaxInt(parseInt(minS.slice(1)), null, out, lessDecimals, false);
- out.push(")");
- if (c < "9") {
- out.push(" | ");
- digitRange(String.fromCharCode(c.charCodeAt(0) + 1), "9");
- out.push(" ");
- moreDigits(length - 1, lessDecimals);
- }
- }
- return;
- }
-
- if (hasMax) {
- if (maxValue >= 0) {
- if (topLevel) {
- out.push("\"-\" [1-9] ");
- moreDigits(0, lessDecimals);
- out.push(" | ");
- }
- _generateMinMaxInt(0, maxValue, out, decimalsLeft, true);
- } else {
- out.push("\"-\" (");
- _generateMinMaxInt(-maxValue, null, out, decimalsLeft, false);
- out.push(")");
- }
- return;
- }
-
- throw new Error("At least one of minValue or maxValue must be set");
-}
-
-class BuiltinRule {
- constructor(content, deps) {
- this.content = content;
- this.deps = deps || [];
- }
-}
-
-const PRIMITIVE_RULES = {
- boolean : new BuiltinRule('("true" | "false") space', []),
- 'decimal-part' : new BuiltinRule('[0-9]{1,16}', []),
- 'integral-part': new BuiltinRule('[0] | [1-9] [0-9]{0,15}', []),
- number : new BuiltinRule('("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space', ['integral-part', 'decimal-part']),
- integer : new BuiltinRule('("-"? integral-part) space', ['integral-part']),
- value : new BuiltinRule('object | array | string | number | boolean | null', ['object', 'array', 'string', 'number', 'boolean', 'null']),
- object : new BuiltinRule('"{" space ( string ":" space value ("," space string ":" space value)* )? "}" space', ['string', 'value']),
- array : new BuiltinRule('"[" space ( value ("," space value)* )? "]" space', ['value']),
- uuid : new BuiltinRule('"\\"" [0-9a-fA-F]{8} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{12} "\\"" space', []),
- char : new BuiltinRule(`[^"\\\\\\x7F\\x00-\\x1F] | [\\\\] (["\\\\bfnrt] | "u" [0-9a-fA-F]{4})`, []),
- string : new BuiltinRule(`"\\"" char* "\\"" space`, ['char']),
- null : new BuiltinRule('"null" space', []),
-};
-
-// TODO: support "uri", "email" string formats
-const STRING_FORMAT_RULES = {
- 'date' : new BuiltinRule('[0-9]{4} "-" ( "0" [1-9] | "1" [0-2] ) "-" ( \"0\" [1-9] | [1-2] [0-9] | "3" [0-1] )', []),
- 'time' : new BuiltinRule('([01] [0-9] | "2" [0-3]) ":" [0-5] [0-9] ":" [0-5] [0-9] ( "." [0-9]{3} )? ( "Z" | ( "+" | "-" ) ( [01] [0-9] | "2" [0-3] ) ":" [0-5] [0-9] )', []),
- 'date-time' : new BuiltinRule('date "T" time', ['date', 'time']),
- 'date-string' : new BuiltinRule('"\\"" date "\\"" space', ['date']),
- 'time-string' : new BuiltinRule('"\\"" time "\\"" space', ['time']),
- 'date-time-string': new BuiltinRule('"\\"" date-time "\\"" space', ['date-time']),
-}
-
-const RESERVED_NAMES = {'root': true, ...PRIMITIVE_RULES, ...STRING_FORMAT_RULES};
-
-const INVALID_RULE_CHARS_RE = /[^\dA-Za-z-]+/g;
-const GRAMMAR_LITERAL_ESCAPE_RE = /[\n\r"]/g;
-const GRAMMAR_RANGE_LITERAL_ESCAPE_RE = /[\n\r"\]\-\\]/g;
-const GRAMMAR_LITERAL_ESCAPES = { '\r': '\\r', '\n': '\\n', '"': '\\"', '-': '\\-', ']': '\\]' };
-
-const NON_LITERAL_SET = new Set('|.()[]{}*+?');
-const ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = new Set('^$.[]()|{}*+?');
-
-export class SchemaConverter {
- constructor(options) {
- this._propOrder = options.prop_order || {};
- this._allowFetch = options.allow_fetch || false;
- this._dotall = options.dotall || false;
- this._rules = {'space': SPACE_RULE};
- this._refs = {};
- this._refsBeingResolved = new Set();
- }
-
- _formatLiteral(literal) {
- const escaped = literal.replace(
- GRAMMAR_LITERAL_ESCAPE_RE,
- m => GRAMMAR_LITERAL_ESCAPES[m]
- );
- return `"${escaped}"`;
- }
-
- _formatRangeChar(literal) {
- return JSON.stringify(literal).slice(1, -1).replace(
- GRAMMAR_RANGE_LITERAL_ESCAPE_RE,
- m => GRAMMAR_LITERAL_ESCAPES[m]
- );
- }
-
- _addRule(name, rule) {
- let escName = name.replace(INVALID_RULE_CHARS_RE, '-');
- let key = escName;
-
- if (escName in this._rules) {
- if (this._rules[escName] === rule) {
- return key;
- }
-
- let i = 0;
- while ((`${escName}${i}` in this._rules) && (this._rules[`${escName}${i}`] !== rule)) {
- i += 1;
- }
- key = `${escName}${i}`;
- }
-
- this._rules[key] = rule;
- return key;
- }
-
- async resolveRefs(schema, url) {
- const visit = async (n) => {
- if (Array.isArray(n)) {
- return Promise.all(n.map(visit));
- } else if (typeof n === 'object' && n !== null) {
- let ref = n.$ref;
- let target;
- if (ref !== undefined && !this._refs[ref]) {
- if (ref.startsWith('https://')) {
- if (!this._allowFetch) {
- throw new Error('Fetching remote schemas is not allowed (use --allow-fetch for force)');
- }
- const fetch = (await import('node-fetch')).default;
-
- const fragSplit = ref.split('#');
- const baseUrl = fragSplit[0];
-
- target = this._refs[baseUrl];
- if (!target) {
- target = await this.resolveRefs(await fetch(ref).then(res => res.json()), baseUrl);
- this._refs[baseUrl] = target;
- }
-
- if (fragSplit.length === 1 || fragSplit[fragSplit.length - 1] === '') {
- return target;
- }
- } else if (ref.startsWith('#/')) {
- target = schema;
- ref = `${url}${ref}`;
- n.$ref = ref;
- } else {
- throw new Error(`Unsupported ref ${ref}`);
- }
-
- const selectors = ref.split('#')[1].split('/').slice(1);
- for (const sel of selectors) {
- if (!target || !(sel in target)) {
- throw new Error(`Error resolving ref ${ref}: ${sel} not in ${JSON.stringify(target)}`);
- }
- target = target[sel];
- }
-
- this._refs[ref] = target;
- } else {
- await Promise.all(Object.values(n).map(visit));
- }
- }
-
- return n;
- };
-
- return visit(schema);
- }
-
- _generateUnionRule(name, altSchemas) {
- return altSchemas
- .map((altSchema, i) => this.visit(altSchema, `${name ?? ''}${name ? '-' : 'alternative-'}${i}`))
- .join(' | ');
- }
-
- _visitPattern(pattern, name) {
- if (!pattern.startsWith('^') || !pattern.endsWith('$')) {
- throw new Error('Pattern must start with "^" and end with "$"');
- }
- pattern = pattern.slice(1, -1);
- const subRuleIds = {};
-
- let i = 0;
- const length = pattern.length;
-
- const getDot = () => {
- let rule;
- if (this._dotall) {
- rule = '[\\U00000000-\\U0010FFFF]';
- } else {
- // Accept any character... except \n and \r line break chars (\x0A and \xOD)
- rule = '[^\\x0A\\x0D]';
- }
- return this._addRule('dot', rule);
- };
-
-
- const toRule = ([s, isLiteral]) => isLiteral ? "\"" + s + "\"" : s;
-
- const transform = () => {
- const start = i;
- // For each component of this sequence, store its string representation and whether it's a literal.
- // We only need a flat structure here to apply repetition operators to the last item, and
- // to merge literals at the and (we're parsing grouped ( sequences ) recursively and don't treat '|' specially
- // (GBNF's syntax is luckily very close to regular expressions!)
- const seq = [];
-
- const joinSeq = () => {
- const ret = [];
- for (const [isLiteral, g] of groupBy(seq, x => x[1])) {
- if (isLiteral) {
- ret.push([[...g].map(x => x[0]).join(''), true]);
- } else {
- ret.push(...g);
- }
- }
- if (ret.length === 1) {
- return ret[0];
- }
- return [ret.map(x => toRule(x)).join(' '), false];
- };
-
- while (i < length) {
- const c = pattern[i];
- if (c === '.') {
- seq.push([getDot(), false]);
- i += 1;
- } else if (c === '(') {
- i += 1;
- if (i < length) {
- if (pattern[i] === '?') {
- throw new Error(`Unsupported pattern syntax "${pattern[i]}" at index ${i} of /${pattern}/`);
- }
- }
- seq.push([`(${toRule(transform())})`, false]);
- } else if (c === ')') {
- i += 1;
- if (start <= 0 || pattern[start - 1] !== '(') {
- throw new Error(`Unbalanced parentheses; start = ${start}, i = ${i}, pattern = ${pattern}`);
- }
- return joinSeq();
- } else if (c === '[') {
- let squareBrackets = c;
- i += 1;
- while (i < length && pattern[i] !== ']') {
- if (pattern[i] === '\\') {
- squareBrackets += pattern.slice(i, i + 2);
- i += 2;
- } else {
- squareBrackets += pattern[i];
- i += 1;
- }
- }
- if (i >= length) {
- throw new Error(`Unbalanced square brackets; start = ${start}, i = ${i}, pattern = ${pattern}`);
- }
- squareBrackets += ']';
- i += 1;
- seq.push([squareBrackets, false]);
- } else if (c === '|') {
- seq.push(['|', false]);
- i += 1;
- } else if (c === '*' || c === '+' || c === '?') {
- seq[seq.length - 1] = [toRule(seq[seq.length - 1]) + c, false];
- i += 1;
- } else if (c === '{') {
- let curlyBrackets = c;
- i += 1;
- while (i < length && pattern[i] !== '}') {
- curlyBrackets += pattern[i];
- i += 1;
- }
- if (i >= length) {
- throw new Error(`Unbalanced curly brackets; start = ${start}, i = ${i}, pattern = ${pattern}`);
- }
- curlyBrackets += '}';
- i += 1;
- const nums = curlyBrackets.slice(1, -1).split(',').map(s => s.trim());
- let minTimes, maxTimes;
- if (nums.length === 1) {
- minTimes = parseInt(nums[0], 10);
- maxTimes = minTimes;
- } else {
- if (nums.length !== 2) {
- throw new Error(`Invalid quantifier ${curlyBrackets}`);
- }
- minTimes = nums[0] ? parseInt(nums[0], 10) : 0;
- maxTimes = nums[1] ? parseInt(nums[1], 10) : Infinity;
- }
-
- let [sub, subIsLiteral] = seq[seq.length - 1];
-
- if (!subIsLiteral) {
- let id = subRuleIds[sub];
- if (id === undefined) {
- id = this._addRule(`${name}-${Object.keys(subRuleIds).length + 1}`, sub);
- subRuleIds[sub] = id;
- }
- sub = id;
- }
-
- seq[seq.length - 1] = [
- _buildRepetition(subIsLiteral ? `"${sub}"` : sub, minTimes, maxTimes, {itemRuleIsLiteral: subIsLiteral}),
- false
- ];
- } else {
- let literal = '';
- while (i < length) {
- if (pattern[i] === '\\' && i < length - 1) {
- const next = pattern[i + 1];
- if (ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS.has(next)) {
- i += 1;
- literal += pattern[i];
- i += 1;
- } else {
- literal += pattern.slice(i, i + 2);
- i += 2;
- }
- } else if (pattern[i] === '"') {
- literal += '\\"';
- i += 1;
- } else if (!NON_LITERAL_SET.has(pattern[i]) &&
- (i === length - 1 || literal === '' || pattern[i + 1] === '.' || !NON_LITERAL_SET.has(pattern[i+1]))) {
- literal += pattern[i];
- i += 1;
- } else {
- break;
- }
- }
- if (literal !== '') {
- seq.push([literal, true]);
- }
- }
- }
-
- return joinSeq();
- };
-
- return this._addRule(name, "\"\\\"\" " + toRule(transform()) + " \"\\\"\" space")
- }
-
- _notStrings(strings) {
- class TrieNode {
- constructor() {
- this.children = {};
- this.isEndOfString = false;
- }
-
- insert(str) {
- let node = this;
- for (const c of str) {
- node = node.children[c] = node.children[c] || new TrieNode();
- }
- node.isEndOfString = true;
- }
- }
-
- const trie = new TrieNode();
- for (const s of strings) {
- trie.insert(s);
- }
-
- const charRuleName = this._addPrimitive('char', PRIMITIVE_RULES['char']);
- const out = ['["] ( '];
-
- const visit = (node) => {
- const rejects = [];
- let first = true;
- for (const c of Object.keys(node.children).sort()) {
- const child = node.children[c];
- rejects.push(c);
- if (first) {
- first = false;
- } else {
- out.push(' | ');
- }
- out.push(`[${c}]`);
- if (Object.keys(child.children).length > 0) {
- out.push(' (');
- visit(child);
- out.push(')');
- } else if (child.isEndOfString) {
- out.push(` ${charRuleName}+`);
- }
- }
- if (Object.keys(node.children).length > 0) {
- if (!first) {
- out.push(' | ');
- }
- out.push(`[^"${rejects.join('')}] ${charRuleName}*`);
- }
- };
-
- visit(trie);
-
- out.push(` )${trie.isEndOfString ? '' : '?'} ["] space`);
- return out.join('');
- }
-
- _resolveRef(ref) {
- let refName = ref.split('/').pop();
- if (!(refName in this._rules) && !this._refsBeingResolved.has(ref)) {
- this._refsBeingResolved.add(ref);
- const resolved = this._refs[ref];
- refName = this.visit(resolved, refName);
- this._refsBeingResolved.delete(ref);
- }
- return refName;
- }
-
- _generateConstantRule(value) {
- return this._formatLiteral(JSON.stringify(value));
- }
-
- visit(schema, name) {
- const schemaType = schema.type;
- const schemaFormat = schema.format;
- const ruleName = name in RESERVED_NAMES ? name + '-' : name == '' ? 'root' : name;
-
- const ref = schema.$ref;
- if (ref !== undefined) {
- return this._addRule(ruleName, this._resolveRef(ref));
- } else if (schema.oneOf || schema.anyOf) {
- return this._addRule(ruleName, this._generateUnionRule(name, schema.oneOf || schema.anyOf));
- } else if (Array.isArray(schemaType)) {
- return this._addRule(ruleName, this._generateUnionRule(name, schemaType.map(t => ({...schema, type: t}))));
- } else if ('const' in schema) {
- return this._addRule(ruleName, this._generateConstantRule(schema.const) + ' space');
- } else if ('enum' in schema) {
- const rule = '(' + schema.enum.map(v => this._generateConstantRule(v)).join(' | ') + ') space';
- return this._addRule(ruleName, rule);
- } else if ((schemaType === undefined || schemaType === 'object') &&
- ('properties' in schema ||
- ('additionalProperties' in schema && schema.additionalProperties !== true))) {
- const required = new Set(schema.required || []);
- const properties = Object.entries(schema.properties ?? {});
- return this._addRule(ruleName, this._buildObjectRule(properties, required, name, schema.additionalProperties));
- } else if ((schemaType === undefined || schemaType === 'object') && 'allOf' in schema) {
- const required = new Set();
- const properties = [];
- const addComponent = (compSchema, isRequired) => {
- const ref = compSchema.$ref;
- if (ref !== undefined) {
- compSchema = this._refs[ref];
- }
-
- if ('properties' in compSchema) {
- for (const [propName, propSchema] of Object.entries(compSchema.properties)) {
- properties.push([propName, propSchema]);
- if (isRequired) {
- required.add(propName);
- }
- }
- }
- };
-
- for (const t of schema.allOf) {
- if ('anyOf' in t) {
- for (const tt of t.anyOf) {
- addComponent(tt, false);
- }
- } else {
- addComponent(t, true);
- }
- }
-
- return this._addRule(ruleName, this._buildObjectRule(properties, required, name, null));
- } else if ((schemaType === undefined || schemaType === 'array') && ('items' in schema || 'prefixItems' in schema)) {
- const items = schema.items ?? schema.prefixItems;
- if (Array.isArray(items)) {
- return this._addRule(
- ruleName,
- '"[" space ' +
- items.map((item, i) => this.visit(item, `${name ?? ''}${name ? '-' : ''}tuple-${i}`)).join(' "," space ') +
- ' "]" space'
- );
- } else {
- const itemRuleName = this.visit(items, `${name ?? ''}${name ? '-' : ''}item`);
- const minItems = schema.minItems || 0;
- const maxItems = schema.maxItems;
- return this._addRule(ruleName, '"[" space ' + _buildRepetition(itemRuleName, minItems, maxItems, {separatorRule: '"," space'}) + ' "]" space');
- }
- } else if ((schemaType === undefined || schemaType === 'string') && 'pattern' in schema) {
- return this._visitPattern(schema.pattern, ruleName);
- } else if ((schemaType === undefined || schemaType === 'string') && /^uuid[1-5]?$/.test(schema.format || '')) {
- return this._addPrimitive(
- ruleName === 'root' ? 'root' : schemaFormat,
- PRIMITIVE_RULES['uuid']
- );
- } else if ((schemaType === undefined || schemaType === 'string') && `${schema.format}-string` in STRING_FORMAT_RULES) {
- const primName = `${schema.format}-string`
- return this._addRule(ruleName, this._addPrimitive(primName, STRING_FORMAT_RULES[primName]));
- } else if (schemaType === 'string' && ('minLength' in schema || 'maxLength' in schema)) {
- const charRuleName = this._addPrimitive('char', PRIMITIVE_RULES['char']);
- const minLen = schema.minLength || 0;
- const maxLen = schema.maxLength;
- return this._addRule(ruleName, '"\\\"" ' + _buildRepetition(charRuleName, minLen, maxLen) + ' "\\\"" space');
- } else if (schemaType === 'integer' && ('minimum' in schema || 'exclusiveMinimum' in schema || 'maximum' in schema || 'exclusiveMaximum' in schema)) {
- let minValue = null;
- let maxValue = null;
- if ('minimum' in schema) {
- minValue = schema.minimum;
- } else if ('exclusiveMinimum' in schema) {
- minValue = schema.exclusiveMinimum + 1;
- }
- if ('maximum' in schema) {
- maxValue = schema.maximum;
- } else if ('exclusiveMaximum' in schema) {
- maxValue = schema.exclusiveMaximum - 1;
- }
-
- const out = ["("];
- _generateMinMaxInt(minValue, maxValue, out);
- out.push(") space");
- return this._addRule(ruleName, out.join(''));
- } else if ((schemaType === 'object') || (Object.keys(schema).length === 0)) {
- return this._addRule(ruleName, this._addPrimitive('object', PRIMITIVE_RULES['object']));
- } else {
- if (!(schemaType in PRIMITIVE_RULES)) {
- throw new Error(`Unrecognized schema: ${JSON.stringify(schema)}`);
- }
- // TODO: support minimum, maximum, exclusiveMinimum, exclusiveMaximum at least for zero
- return this._addPrimitive(ruleName === 'root' ? 'root' : schemaType, PRIMITIVE_RULES[schemaType]);
- }
- }
-
- _addPrimitive(name, rule) {
- let n = this._addRule(name, rule.content);
- for (const dep of rule.deps) {
- const depRule = PRIMITIVE_RULES[dep] || STRING_FORMAT_RULES[dep];
- if (!depRule) {
- throw new Error(`Rule ${dep} not known`);
- }
- if (!(dep in this._rules)) {
- this._addPrimitive(dep, depRule);
- }
- }
- return n;
- }
-
- _buildObjectRule(properties, required, name, additionalProperties) {
- const propOrder = this._propOrder;
- // sort by position in prop_order (if specified) then by original order
- const sortedProps = properties.map(([k]) => k).sort((a, b) => {
- const orderA = propOrder[a] || Infinity;
- const orderB = propOrder[b] || Infinity;
- return orderA - orderB || properties.findIndex(([k]) => k === a) - properties.findIndex(([k]) => k === b);
- });
-
- const propKvRuleNames = {};
- for (const [propName, propSchema] of properties) {
- const propRuleName = this.visit(propSchema, `${name ?? ''}${name ? '-' : ''}${propName}`);
- propKvRuleNames[propName] = this._addRule(
- `${name ?? ''}${name ? '-' : ''}${propName}-kv`,
- `${this._formatLiteral(JSON.stringify(propName))} space ":" space ${propRuleName}`
- );
- }
- const requiredProps = sortedProps.filter(k => required.has(k));
- const optionalProps = sortedProps.filter(k => !required.has(k));
-
- if (additionalProperties) {
- const subName = `${name ?? ''}${name ? '-' : ''}additional`;
- const valueRule =
- additionalProperties != null && typeof additionalProperties === 'object' ? this.visit(additionalProperties, `${subName}-value`)
- : this._addPrimitive('value', PRIMITIVE_RULES['value']);
-
- const key_rule =
- sortedProps.length === 0 ? this._addPrimitive('string', PRIMITIVE_RULES['string'])
- : this._addRule(`${subName}-k`, this._notStrings(sortedProps));
-
- propKvRuleNames['*'] = this._addRule(
- `${subName}-kv`,
- `${key_rule} ":" space ${valueRule}`);
- optionalProps.push('*');
- }
-
- let rule = '"{" space ';
- rule += requiredProps.map(k => propKvRuleNames[k]).join(' "," space ');
-
- if (optionalProps.length > 0) {
- rule += ' (';
- if (requiredProps.length > 0) {
- rule += ' "," space ( ';
- }
-
- const getRecursiveRefs = (ks, firstIsOptional) => {
- const [k, ...rest] = ks;
- const kvRuleName = propKvRuleNames[k];
- let res;
- const commaRef = `( "," space ${kvRuleName} )`;
- if (firstIsOptional) {
- res = commaRef + (k === '*' ? '*' : '?');
- } else {
- res = kvRuleName + (k === '*' ? ' ' + commaRef + '*' : '');
- }
- if (rest.length > 0) {
- res += ' ' + this._addRule(
- `${name ?? ''}${name ? '-' : ''}${k}-rest`,
- getRecursiveRefs(rest, true)
- );
- }
- return res;
- };
-
- rule += optionalProps.map((_, i) => getRecursiveRefs(optionalProps.slice(i), false)).join(' | ');
- if (requiredProps.length > 0) {
- rule += ' )';
- }
- rule += ' )?';
- }
-
- rule += ' "}" space';
-
- return rule;
- }
-
- formatGrammar() {
- let grammar = '';
- for (const [name, rule] of Object.entries(this._rules).sort(([a], [b]) => a.localeCompare(b))) {
- grammar += `${name} ::= ${rule}\n`;
- }
- return grammar;
- }
-}
-
-// Helper function to group elements by a key function
-function* groupBy(iterable, keyFn) {
- let lastKey = null;
- let group = [];
- for (const element of iterable) {
- const key = keyFn(element);
- if (lastKey !== null && key !== lastKey) {
- yield [lastKey, group];
- group = [];
- }
- group.push(element);
- lastKey = key;
- }
- if (group.length > 0) {
- yield [lastKey, group];
- }
-}