json-schema-to-grammar.mjs 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. const SPACE_RULE = '" "?';
  2. const PRIMITIVE_RULES = {
  3. boolean: '("true" | "false") space',
  4. number: '("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? space',
  5. integer: '("-"? ([0-9] | [1-9] [0-9]*)) space',
  6. string: ` "\\"" (
  7. [^"\\\\] |
  8. "\\\\" (["\\\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
  9. )* "\\"" space`,
  10. null: '"null" space',
  11. };
  12. const INVALID_RULE_CHARS_RE = /[^\dA-Za-z-]+/g;
  13. const GRAMMAR_LITERAL_ESCAPE_RE = /[\n\r"]/g;
  14. const GRAMMAR_LITERAL_ESCAPES = {'\r': '\\r', '\n': '\\n', '"': '\\"'};
  15. export class SchemaConverter {
  16. constructor(propOrder) {
  17. this._propOrder = propOrder || {};
  18. this._rules = new Map();
  19. this._rules.set('space', SPACE_RULE);
  20. }
  21. _formatLiteral(literal) {
  22. const escaped = JSON.stringify(literal).replace(
  23. GRAMMAR_LITERAL_ESCAPE_RE,
  24. m => GRAMMAR_LITERAL_ESCAPES[m]
  25. );
  26. return `"${escaped}"`;
  27. }
  28. _addRule(name, rule) {
  29. let escName = name.replace(INVALID_RULE_CHARS_RE, '-');
  30. let key = escName;
  31. if (this._rules.has(escName)) {
  32. if (this._rules.get(escName) === rule) {
  33. return key;
  34. }
  35. let i = 0;
  36. while (this._rules.has(`${escName}${i}`)) {
  37. i += 1;
  38. }
  39. key = `${escName}${i}`;
  40. }
  41. this._rules.set(key, rule);
  42. return key;
  43. }
  44. visit(schema, name) {
  45. const schemaType = schema.type;
  46. const ruleName = name || 'root';
  47. if (schema.oneOf || schema.anyOf) {
  48. const rule = (schema.oneOf || schema.anyOf).map((altSchema, i) =>
  49. this.visit(altSchema, `${name}${name ? "-" : ""}${i}`)
  50. ).join(' | ');
  51. return this._addRule(ruleName, rule);
  52. } else if ('const' in schema) {
  53. return this._addRule(ruleName, this._formatLiteral(schema.const));
  54. } else if ('enum' in schema) {
  55. const rule = schema.enum.map(v => this._formatLiteral(v)).join(' | ');
  56. return this._addRule(ruleName, rule);
  57. } else if (schemaType === 'object' && 'properties' in schema) {
  58. // TODO: `required` keyword (from python implementation)
  59. const propOrder = this._propOrder;
  60. const propPairs = Object.entries(schema.properties).sort((a, b) => {
  61. // sort by position in prop_order (if specified) then by key
  62. const orderA = typeof propOrder[a[0]] === 'number' ? propOrder[a[0]] : Infinity;
  63. const orderB = typeof propOrder[b[0]] === 'number' ? propOrder[b[0]] : Infinity;
  64. return orderA - orderB || a[0].localeCompare(b[0]);
  65. });
  66. let rule = '"{" space';
  67. propPairs.forEach(([propName, propSchema], i) => {
  68. const propRuleName = this.visit(propSchema, `${name}${name ? "-" : ""}${propName}`);
  69. if (i > 0) {
  70. rule += ' "," space';
  71. }
  72. rule += ` ${this._formatLiteral(propName)} space ":" space ${propRuleName}`;
  73. });
  74. rule += ' "}" space';
  75. return this._addRule(ruleName, rule);
  76. } else if (schemaType === 'array' && 'items' in schema) {
  77. // TODO `prefixItems` keyword (from python implementation)
  78. const itemRuleName = this.visit(schema.items, `${name}${name ? "-" : ""}item`);
  79. const rule = `"[" space (${itemRuleName} ("," space ${itemRuleName})*)? "]" space`;
  80. return this._addRule(ruleName, rule);
  81. } else {
  82. if (!PRIMITIVE_RULES[schemaType]) {
  83. throw new Error(`Unrecognized schema: ${JSON.stringify(schema)}`);
  84. }
  85. return this._addRule(
  86. ruleName === 'root' ? 'root' : schemaType,
  87. PRIMITIVE_RULES[schemaType]
  88. );
  89. }
  90. }
  91. formatGrammar() {
  92. let grammar = '';
  93. this._rules.forEach((rule, name) => {
  94. grammar += `${name} ::= ${rule}\n`;
  95. });
  96. return grammar;
  97. }
  98. }