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.mjs112
1 files changed, 112 insertions, 0 deletions
diff --git a/examples/server/public/json-schema-to-grammar.mjs b/examples/server/public/json-schema-to-grammar.mjs
new file mode 100644
index 00000000..3f1b255c
--- /dev/null
+++ b/examples/server/public/json-schema-to-grammar.mjs
@@ -0,0 +1,112 @@
+const SPACE_RULE = '" "?';
+
+const PRIMITIVE_RULES = {
+ boolean: '("true" | "false") space',
+ number: '("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? space',
+ integer: '("-"? ([0-9] | [1-9] [0-9]*)) space',
+ string: ` "\\"" (
+ [^"\\\\] |
+ "\\\\" (["\\\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
+ )* "\\"" space`,
+ null: '"null" space',
+};
+
+const INVALID_RULE_CHARS_RE = /[^\dA-Za-z-]+/g;
+const GRAMMAR_LITERAL_ESCAPE_RE = /[\n\r"]/g;
+const GRAMMAR_LITERAL_ESCAPES = {'\r': '\\r', '\n': '\\n', '"': '\\"'};
+
+export class SchemaConverter {
+ constructor(propOrder) {
+ this._propOrder = propOrder || {};
+ this._rules = new Map();
+ this._rules.set('space', SPACE_RULE);
+ }
+
+ _formatLiteral(literal) {
+ const escaped = JSON.stringify(literal).replace(
+ GRAMMAR_LITERAL_ESCAPE_RE,
+ m => GRAMMAR_LITERAL_ESCAPES[m]
+ );
+ return `"${escaped}"`;
+ }
+
+ _addRule(name, rule) {
+ let escName = name.replace(INVALID_RULE_CHARS_RE, '-');
+ let key = escName;
+
+ if (this._rules.has(escName)) {
+ if (this._rules.get(escName) === rule) {
+ return key;
+ }
+
+ let i = 0;
+ while (this._rules.has(`${escName}${i}`)) {
+ i += 1;
+ }
+ key = `${escName}${i}`;
+ }
+
+ this._rules.set(key, rule);
+ return key;
+ }
+
+ visit(schema, name) {
+ const schemaType = schema.type;
+ const ruleName = name || 'root';
+
+ if (schema.oneOf || schema.anyOf) {
+ const rule = (schema.oneOf || schema.anyOf).map((altSchema, i) =>
+ this.visit(altSchema, `${name}${name ? "-" : ""}${i}`)
+ ).join(' | ');
+
+ return this._addRule(ruleName, rule);
+ } else if ('const' in schema) {
+ return this._addRule(ruleName, this._formatLiteral(schema.const));
+ } else if ('enum' in schema) {
+ const rule = schema.enum.map(v => this._formatLiteral(v)).join(' | ');
+ return this._addRule(ruleName, rule);
+ } else if (schemaType === 'object' && 'properties' in schema) {
+ // TODO: `required` keyword (from python implementation)
+ const propOrder = this._propOrder;
+ const propPairs = Object.entries(schema.properties).sort((a, b) => {
+ // sort by position in prop_order (if specified) then by key
+ const orderA = typeof propOrder[a[0]] === 'number' ? propOrder[a[0]] : Infinity;
+ const orderB = typeof propOrder[b[0]] === 'number' ? propOrder[b[0]] : Infinity;
+ return orderA - orderB || a[0].localeCompare(b[0]);
+ });
+
+ let rule = '"{" space';
+ propPairs.forEach(([propName, propSchema], i) => {
+ const propRuleName = this.visit(propSchema, `${name}${name ? "-" : ""}${propName}`);
+ if (i > 0) {
+ rule += ' "," space';
+ }
+ rule += ` ${this._formatLiteral(propName)} space ":" space ${propRuleName}`;
+ });
+ rule += ' "}" space';
+
+ return this._addRule(ruleName, rule);
+ } else if (schemaType === 'array' && 'items' in schema) {
+ // TODO `prefixItems` keyword (from python implementation)
+ const itemRuleName = this.visit(schema.items, `${name}${name ? "-" : ""}item`);
+ const rule = `"[" space (${itemRuleName} ("," space ${itemRuleName})*)? "]" space`;
+ return this._addRule(ruleName, rule);
+ } else {
+ if (!PRIMITIVE_RULES[schemaType]) {
+ throw new Error(`Unrecognized schema: ${JSON.stringify(schema)}`);
+ }
+ return this._addRule(
+ ruleName === 'root' ? 'root' : schemaType,
+ PRIMITIVE_RULES[schemaType]
+ );
+ }
+ }
+
+ formatGrammar() {
+ let grammar = '';
+ this._rules.forEach((rule, name) => {
+ grammar += `${name} ::= ${rule}\n`;
+ });
+ return grammar;
+ }
+}