@xdbml/parse 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/parser.js CHANGED
@@ -13,11 +13,37 @@ import { TokenKind, tokenize, } from "./lexer.js";
13
13
  import { resolveImport, classifyModuleSource, ModuleSourceError } from "./module-resolver.js";
14
14
  export class ParseError extends Error {
15
15
  position;
16
- constructor(message, position) {
16
+ /**
17
+ * Optional machine-readable reason. Set for the errors a caller may want
18
+ * to tell apart from a plain syntax error: `unsupported-version` when a
19
+ * document declares a newer version than this parser supports (spec 4.1).
20
+ */
21
+ code;
22
+ constructor(message, position, code) {
17
23
  super(`${message} (line ${position.line}, column ${position.column})`);
18
24
  this.position = position;
25
+ if (code)
26
+ this.code = code;
19
27
  }
20
28
  }
29
+ /**
30
+ * The newest specification version this parser implements. A document
31
+ * declaring a later version is refused (spec 4.1) rather than parsed with
32
+ * semantics it does not have.
33
+ */
34
+ export const SUPPORTED_XDBML_VERSION = '0.5';
35
+ /** Compare dotted version strings numerically: -1, 0 or 1. */
36
+ export function compareVersions(a, b) {
37
+ const pa = a.split('.').map((n) => Number(n) || 0);
38
+ const pb = b.split('.').map((n) => Number(n) || 0);
39
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
40
+ const x = pa[i] ?? 0;
41
+ const y = pb[i] ?? 0;
42
+ if (x !== y)
43
+ return x < y ? -1 : 1;
44
+ }
45
+ return 0;
46
+ }
21
47
  /* -------------------------------------------------------------------------
22
48
  * Keyword recognition.
23
49
  *
@@ -31,13 +57,13 @@ const CONTAINER_KEYWORDS = new Set([
31
57
  const ENTITY_KEYWORDS = new Set(['table', 'entity', 'collection', 'record']);
32
58
  /**
33
59
  * Element-type keywords accepted in module-system import items
34
- * (spec §26.3). Stored lowercased; matching is case-insensitive.
60
+ * (spec §27.3). Stored lowercased; matching is case-insensitive.
35
61
  *
36
62
  * `field` is recognized but explicitly rejected by parseImportItem in P4
37
63
  * (field-level imports have special declaration-vs-placement semantics
38
64
  * that will land in a later batch).
39
65
  *
40
- * `project` is intentionally excluded -- spec §26.1 forbids importing
66
+ * `project` is intentionally excluded -- spec §27.1 forbids importing
41
67
  * Project declarations.
42
68
  */
43
69
  const IMPORT_ELEMENT_TYPES = new Set([
@@ -45,7 +71,7 @@ const IMPORT_ELEMENT_TYPES = new Set([
45
71
  'enum', 'tablepartial', 'note',
46
72
  'schema', 'container', 'tablegroup',
47
73
  'type', 'edge', 'view', 'diagramview',
48
- 'field',
74
+ 'field', 'supertypegroup',
49
75
  ]);
50
76
  const STRUCTURAL_TYPE_KEYWORDS = new Set([
51
77
  'object', 'struct', 'record', 'array', 'list', 'map', 'dict', 'dictionary',
@@ -98,7 +124,7 @@ export class Parser {
98
124
  * The set of file paths currently being parsed in the resolution chain.
99
125
  * Used for cycle detection: when resolving a directive whose `from` path
100
126
  * is already in this set, the parser produces an empty clone for that
101
- * directive rather than recursing (matching spec §26.15: cycles are
127
+ * directive rather than recursing (matching spec §27.15: cycles are
102
128
  * allowed; name resolution handles them). The set is passed by reference
103
129
  * across recursive parse() calls so all transitive levels see it.
104
130
  *
@@ -179,6 +205,11 @@ export class Parser {
179
205
  this.advance(); // xdbml
180
206
  this.expect(TokenKind.Colon, "Expected ':' after 'xdbml'");
181
207
  const numTok = this.expect(TokenKind.NumberLiteral, 'Expected version number');
208
+ if (compareVersions(numTok.text, SUPPORTED_XDBML_VERSION) > 0) {
209
+ throw new ParseError(`This document declares 'xdbml: ${numTok.text}', which is newer than the ` +
210
+ `latest version this parser supports (${SUPPORTED_XDBML_VERSION}). ` +
211
+ 'Use a newer parser, or declare a supported version.', numTok.start, 'unsupported-version');
212
+ }
182
213
  return {
183
214
  kind: 'VersionDeclaration',
184
215
  version: numTok.text,
@@ -231,6 +262,8 @@ export class Parser {
231
262
  return this.parseTablePartial();
232
263
  if (k === 'tablegroup')
233
264
  return this.parseTableGroup();
265
+ if (k === 'supertypegroup')
266
+ return this.parseSupertypeGroup();
234
267
  if (k === 'note')
235
268
  return this.parseNoteDeclaration();
236
269
  if (k === 'records')
@@ -477,7 +510,7 @@ export class Parser {
477
510
  };
478
511
  }
479
512
  /**
480
- * Parse a `records { ... }` block inside an entity body (§25.1, implicit
513
+ * Parse a `records { ... }` block inside an entity body (§26.1, implicit
481
514
  * column list). Values are stored as SettingValue cells; row boundaries
482
515
  * are determined by source line (see `parseRecordRow`).
483
516
  */
@@ -497,7 +530,7 @@ export class Parser {
497
530
  };
498
531
  }
499
532
  /**
500
- * Top-level records declaration (§25.2, new in v0.2):
533
+ * Top-level records declaration (§26.2, new in v0.2):
501
534
  *
502
535
  * records users (id, name, email) { ... }
503
536
  * records core.users (id, name, email) { ... }
@@ -587,7 +620,7 @@ export class Parser {
587
620
  span: this.spanFrom(start),
588
621
  };
589
622
  }
590
- /* ----- Module-system directives (spec §26, new in v0.2) ----- */
623
+ /* ----- Module-system directives (spec §27, new in v0.2) ----- */
591
624
  /**
592
625
  * Parse a `use` or `reuse` directive. Called from both the top-level
593
626
  * dispatcher and the Container body dispatcher; the caller indicates
@@ -701,7 +734,7 @@ export class Parser {
701
734
  resolvedPath = result.resolvedPath;
702
735
  break;
703
736
  case 'cycle':
704
- // Per spec §26.15, cycles are allowed; the parser produces a
737
+ // Per spec §27.15, cycles are allowed; the parser produces a
705
738
  // directive with no clone, and name resolution (P6+) is
706
739
  // expected to bridge the cycle. We leave clone undefined.
707
740
  resolvedPath = result.resolvedPath;
@@ -759,12 +792,12 @@ export class Parser {
759
792
  `Expected one of: ${Array.from(IMPORT_ELEMENT_TYPES).join(', ')}.`, elemTok.start);
760
793
  }
761
794
  if (elementType === 'field' && context !== 'file-scope') {
762
- // Spec §26.8: field imports must appear at file scope. Inside a
795
+ // Spec §27.8: field imports must appear at file scope. Inside a
763
796
  // Container body, the field's eventual placement (as a Named Type)
764
797
  // would have no meaningful container scope -- field imports are
765
798
  // always lifted to file scope by flatten(), regardless of where
766
799
  // the directive sits.
767
- throw new ParseError(`Field-level imports must appear at file scope, not inside a Container body (spec §26.8).`, elemTok.start);
800
+ throw new ParseError(`Field-level imports must appear at file scope, not inside a Container body (spec §27.8).`, elemTok.start);
768
801
  }
769
802
  this.advance(); // consume element type keyword
770
803
  // Dotted source path.
@@ -795,12 +828,12 @@ export class Parser {
795
828
  * that match the import items by name and element type (matching is
796
829
  * downstream-consumer's job; the parser is permissive).
797
830
  *
798
- * Per spec §26.6, clone content uses the importing file's vocabulary
831
+ * Per spec §27.6, clone content uses the importing file's vocabulary
799
832
  * (aliases already applied) and is parsed under the importing file's
800
833
  * xdbml version directive.
801
834
  *
802
835
  * Most clone-block content uses TopLevelStatement shapes (Entity, Type,
803
- * Container, etc.). The exception is field imports (§26.8): when the
836
+ * Container, etc.). The exception is field imports (§27.8): when the
804
837
  * directive imports one or more fields via `field <path>` items, the
805
838
  * clone block holds each field as a bare FieldDeclaration with no entity
806
839
  * wrapper. The dispatch below checks whether the next token starts a
@@ -816,7 +849,7 @@ export class Parser {
816
849
  statements.push(this.parseTopLevelStatement());
817
850
  }
818
851
  else {
819
- // Bare field declaration -- the field-import case. Per spec §26.6
852
+ // Bare field declaration -- the field-import case. Per spec §27.6
820
853
  // the field appears without an entity wrapper.
821
854
  statements.push(this.parseFieldDeclaration());
822
855
  }
@@ -1284,7 +1317,7 @@ export class Parser {
1284
1317
  }
1285
1318
  throw new ParseError(`Expected type parameter, got ${t.kind}`, t.start);
1286
1319
  }
1287
- /* ----- Type declaration (§13) ----- */
1320
+ /* ----- Type declaration (§15) ----- */
1288
1321
  parseTypeDecl() {
1289
1322
  const start = this.peek().start;
1290
1323
  this.advance(); // Type
@@ -1293,7 +1326,7 @@ export class Parser {
1293
1326
  //
1294
1327
  // { ... } v0.1 object form, no pre-body settings
1295
1328
  // [ settings ] { ... } v0.1 object form, pre-body settings (permissive)
1296
- // typeExpression v0.2 scalar form (spec §14.7)
1329
+ // typeExpression v0.2 scalar form (spec §15.7)
1297
1330
  // typeExpression [ settings ] v0.2 scalar form with field-level settings
1298
1331
  //
1299
1332
  // Note that LBrace and LBracket are distinct from any start-of-type-expression
@@ -1572,7 +1605,7 @@ export class Parser {
1572
1605
  };
1573
1606
  }
1574
1607
  /**
1575
- * Parse a dotted path with the §18 segment vocabulary:
1608
+ * Parse a dotted path with the §20 segment vocabulary:
1576
1609
  *
1577
1610
  * IDENTIFIER -- a field segment
1578
1611
  * .IDENTIFIER -- field
@@ -1746,6 +1779,67 @@ export class Parser {
1746
1779
  span: this.spanFrom(start),
1747
1780
  };
1748
1781
  }
1782
+ /* ----- SupertypeGroup (spec §12, new in v0.5) ----- */
1783
+ /**
1784
+ * `SupertypeGroup <name> [supertype: X, ...] { Sub1 Sub2 [strategy: y] }`.
1785
+ * Members are separated like TableGroup members: newline, comma or
1786
+ * semicolon (spec §3.9). A name is required (§12.1); a writer exporting a
1787
+ * group that has none emits `undefinedGroup1`, `undefinedGroup2`, ... so
1788
+ * the parser never supplies one. Values are validated after parsing, by
1789
+ * `checkSupertypeGroups()`, so an unknown value gets a located diagnostic
1790
+ * rather than stopping the parse.
1791
+ */
1792
+ parseSupertypeGroup() {
1793
+ const start = this.peek().start;
1794
+ this.advance(); // SupertypeGroup
1795
+ const nameTok = this.peek();
1796
+ if (nameTok.kind !== TokenKind.Identifier && nameTok.kind !== TokenKind.QuotedIdentifier) {
1797
+ throw new ParseError('Expected a name after SupertypeGroup. Every group has a name (spec §12.1); ' +
1798
+ 'a group exported without one is written undefinedGroup1, undefinedGroup2, ...', nameTok.start);
1799
+ }
1800
+ const name = this.parseIdentLikeName('SupertypeGroup name');
1801
+ const settings = this.maybeSettingsBlock();
1802
+ this.expect(TokenKind.LBrace, "Expected '{' after SupertypeGroup name and settings");
1803
+ const members = [];
1804
+ while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1805
+ const t = this.peek();
1806
+ if (t.kind === TokenKind.Identifier || t.kind === TokenKind.QuotedIdentifier) {
1807
+ const memberStart = t.start;
1808
+ this.advance();
1809
+ let n = t.kind === TokenKind.QuotedIdentifier ? (t.value ?? '') : t.text;
1810
+ while (this.check(TokenKind.Dot)) {
1811
+ this.advance();
1812
+ const next = this.peek();
1813
+ if (next.kind !== TokenKind.Identifier && next.kind !== TokenKind.QuotedIdentifier) {
1814
+ throw new ParseError('Expected identifier after dot in subtype path', next.start);
1815
+ }
1816
+ this.advance();
1817
+ n += `.${next.kind === TokenKind.QuotedIdentifier ? (next.value ?? '') : next.text}`;
1818
+ }
1819
+ const memberSettings = this.maybeSettingsBlock();
1820
+ members.push({
1821
+ kind: 'SupertypeGroupMember',
1822
+ name: n,
1823
+ settings: memberSettings,
1824
+ span: this.spanFrom(memberStart),
1825
+ });
1826
+ }
1827
+ else if (t.kind === TokenKind.Semicolon || t.kind === TokenKind.Comma) {
1828
+ this.advance();
1829
+ }
1830
+ else {
1831
+ throw new ParseError(`Unexpected ${t.kind} in SupertypeGroup body; expected a subtype entity name`, t.start);
1832
+ }
1833
+ }
1834
+ this.expect(TokenKind.RBrace, "Expected '}' closing SupertypeGroup");
1835
+ return {
1836
+ kind: 'SupertypeGroupDeclaration',
1837
+ name,
1838
+ settings,
1839
+ members,
1840
+ span: this.spanFrom(start),
1841
+ };
1842
+ }
1749
1843
  /* ----- Indexes ----- */
1750
1844
  parseIndexes() {
1751
1845
  const start = this.peek().start;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Supertype groups (spec §12, new in v0.5).
3
+ *
4
+ * Three things live here:
5
+ *
6
+ * - Setting normalization. `completeness`, `exclusivity`, `strategy` and
7
+ * `merge` accept a canonical value and a set of aliases (spec §12.2).
8
+ * The AST keeps what was written; `supertypeGroupSettings()` returns
9
+ * the canonical values.
10
+ * - Member resolution. The supertype and every subtype name an entity by
11
+ * path, bare (`Person`) or container-qualified (`crm.Person`), with the
12
+ * same reading as an entity-level `Ref` endpoint (spec §11.16.3).
13
+ * - The validation rules summarized in spec §12.8, run by `resolveNames()`
14
+ * on the flattened document so imported and cloned groups are checked
15
+ * like local ones.
16
+ *
17
+ * Nothing here computes the attributes or keys a subtype would hold in a
18
+ * physical model. A subtype declares its own attributes only; placing the
19
+ * supertype's attributes in stored structures is derivation output (spec
20
+ * §12.5, §12.7). Supertype chains are computed for the structural checks
21
+ * and for tools that display the hierarchy.
22
+ */
23
+ import type { Diagnostic } from './name-resolver.ts';
24
+ import type { SupertypeGroupDeclaration, SupertypeGroupMember, XDbmlDocument } from './ast.ts';
25
+ export type Completeness = 'total' | 'partial';
26
+ export type Exclusivity = 'disjoint' | 'overlapping';
27
+ export type MaterializationStrategy = 'preserved_hierarchy' | 'roll_up' | 'roll_down';
28
+ export type MergeOption = 'flat' | 'nested';
29
+ /**
30
+ * Accepted spellings, keyed by lowercase spelling, mapped to the canonical
31
+ * value. Canonical values map to themselves.
32
+ */
33
+ export declare const SUPERTYPE_GROUP_VALUES: {
34
+ readonly completeness: {
35
+ readonly total: "total";
36
+ readonly complete: "total";
37
+ readonly partial: "partial";
38
+ readonly incomplete: "partial";
39
+ };
40
+ readonly exclusivity: {
41
+ readonly disjoint: "disjoint";
42
+ readonly exclusive: "disjoint";
43
+ readonly overlapping: "overlapping";
44
+ readonly non_exclusive: "overlapping";
45
+ };
46
+ readonly strategy: {
47
+ readonly preserved_hierarchy: "preserved_hierarchy";
48
+ readonly class_table: "preserved_hierarchy";
49
+ readonly joined: "preserved_hierarchy";
50
+ readonly roll_up: "roll_up";
51
+ readonly single_table: "roll_up";
52
+ readonly roll_down: "roll_down";
53
+ readonly concrete_table: "roll_down";
54
+ readonly table_per_class: "roll_down";
55
+ };
56
+ readonly merge: {
57
+ readonly flat: "flat";
58
+ readonly flat_with_discriminator: "flat";
59
+ readonly nested: "nested";
60
+ };
61
+ };
62
+ export type SupertypeGroupValueSetting = keyof typeof SUPERTYPE_GROUP_VALUES;
63
+ /** The canonical value for a spelling, or undefined when it is not recognized. */
64
+ export declare function canonicalSupertypeGroupValue(setting: SupertypeGroupValueSetting, raw: string): string | undefined;
65
+ /** A group's settings with canonical values. Unrecognized values are left out. */
66
+ export interface SupertypeGroupSettings {
67
+ supertype?: string;
68
+ completeness?: Completeness;
69
+ exclusivity?: Exclusivity;
70
+ strategy?: MaterializationStrategy;
71
+ merge?: MergeOption;
72
+ discriminator?: string;
73
+ note?: string;
74
+ }
75
+ export declare function supertypeGroupSettings(decl: SupertypeGroupDeclaration): SupertypeGroupSettings;
76
+ /** A subtype member's own `strategy` (spec §12.7.4), canonical, or undefined. */
77
+ export declare function subtypeStrategy(member: SupertypeGroupMember): MaterializationStrategy | undefined;
78
+ /** One group with its members resolved to entity ids where they resolve. */
79
+ export interface ResolvedSupertypeGroup {
80
+ declaration: SupertypeGroupDeclaration;
81
+ settings: SupertypeGroupSettings;
82
+ /** Entity id of the supertype, when it resolves. */
83
+ supertype?: string;
84
+ subtypes: Array<{
85
+ member: SupertypeGroupMember;
86
+ /** Entity id of the subtype, when it resolves. */
87
+ entity?: string;
88
+ /** The member's own strategy, canonical, when stated and recognized. */
89
+ strategy?: MaterializationStrategy;
90
+ }>;
91
+ }
92
+ /**
93
+ * Every SupertypeGroup of a document with its members resolved. Pass the
94
+ * flattened document (see `flatten()`) so imported and cloned declarations
95
+ * are visible. Entity ids are container-qualified for entities declared in
96
+ * a Container, bare otherwise.
97
+ */
98
+ export declare function resolveSupertypeGroups(doc: XDbmlDocument): ResolvedSupertypeGroup[];
99
+ /**
100
+ * For each entity that is a subtype, its chain of supertypes, nearest
101
+ * first: `Employee -> [Person, Party]`. When an entity is listed as a
102
+ * subtype in several groups (an error), the first group wins; a chain that
103
+ * loops stops before repeating an entity.
104
+ */
105
+ export declare function supertypeChains(doc: XDbmlDocument): Map<string, string[]>;
106
+ /**
107
+ * The rules of spec §12.8. Run by `resolveNames()` on the flattened
108
+ * document; exported for callers that check groups on their own.
109
+ */
110
+ export declare function checkSupertypeGroups(doc: XDbmlDocument): Diagnostic[];