Spaces:
Build error
Build error
| // 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]; | |
| } | |
| } | |