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.mjs306
1 files changed, 290 insertions, 16 deletions
diff --git a/examples/server/public/json-schema-to-grammar.mjs b/examples/server/public/json-schema-to-grammar.mjs
index faed6a32..7267f3f9 100644
--- a/examples/server/public/json-schema-to-grammar.mjs
+++ b/examples/server/public/json-schema-to-grammar.mjs
@@ -24,6 +24,201 @@ function _buildRepetition(itemRule, minItems, maxItems, opts={}) {
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;
@@ -64,7 +259,7 @@ 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('[]()|{}*+?');
+const ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = new Set('^$.[]()|{}*+?');
export class SchemaConverter {
constructor(options) {
@@ -337,6 +532,64 @@ export class SchemaConverter {
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)) {
@@ -363,11 +616,11 @@ export class SchemaConverter {
} 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 => ({ type: t }))));
+ 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));
+ return this._addRule(ruleName, this._generateConstantRule(schema.const) + ' space');
} else if ('enum' in schema) {
- const rule = schema.enum.map(v => this._generateConstantRule(v)).join(' | ');
+ 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 ||
@@ -404,7 +657,7 @@ export class SchemaConverter {
}
}
- return this._addRule(ruleName, this._buildObjectRule(properties, required, name, /* additionalProperties= */ false));
+ 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)) {
@@ -435,6 +688,24 @@ export class SchemaConverter {
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 {
@@ -480,12 +751,19 @@ export class SchemaConverter {
const requiredProps = sortedProps.filter(k => required.has(k));
const optionalProps = sortedProps.filter(k => !required.has(k));
- if (typeof additionalProperties === 'object' || additionalProperties === true) {
+ if (additionalProperties) {
const subName = `${name ?? ''}${name ? '-' : ''}additional`;
- const valueRule = this.visit(additionalProperties === true ? {} : additionalProperties, `${subName}-value`);
+ 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`,
- `${this._addPrimitive('string', PRIMITIVE_RULES['string'])} ":" space ${valueRule}`);
+ `${key_rule} ":" space ${valueRule}`);
optionalProps.push('*');
}
@@ -502,15 +780,11 @@ export class SchemaConverter {
const [k, ...rest] = ks;
const kvRuleName = propKvRuleNames[k];
let res;
- if (k === '*') {
- res = this._addRule(
- `${name ?? ''}${name ? '-' : ''}additional-kvs`,
- `${kvRuleName} ( "," space ` + kvRuleName + ` )*`
- )
- } else if (firstIsOptional) {
- res = `( "," space ${kvRuleName} )?`;
+ const commaRef = `( "," space ${kvRuleName} )`;
+ if (firstIsOptional) {
+ res = commaRef + (k === '*' ? '*' : '?');
} else {
- res = kvRuleName;
+ res = kvRuleName + (k === '*' ? ' ' + commaRef + '*' : '');
}
if (rest.length > 0) {
res += ' ' + this._addRule(