2024-01-30 19:15:05 +01:00
|
|
|
const char json_schema_to_grammar_mjs[] = R"LITERAL(
|
|
|
|
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',
|
2023-08-14 09:16:54 +02:00
|
|
|
};
|
2024-01-30 19:15:05 +01:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)LITERAL";
|
|
|
|
unsigned int json_schema_to_grammar_mjs_len = sizeof(json_schema_to_grammar_mjs);
|