@iyulab/m3l 0.1.3 → 0.1.4
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/README.md +66 -10
- package/dist/lexer.js +9 -1
- package/dist/parser.js +28 -1
- package/dist/resolver.d.ts +1 -1
- package/dist/resolver.js +1 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @iyulab/m3l
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@iyulab/m3l)
|
|
4
|
+
[](https://github.com/iyulab/m3l/actions/workflows/parser-publish.yml)
|
|
5
|
+
[](../../LICENSE)
|
|
6
|
+
|
|
7
|
+
M3L (Meta Model Markup Language) parser and CLI — parse `.m3l.md` / `.m3l` files into a structured JSON AST.
|
|
4
8
|
|
|
5
9
|
M3L is a Markdown-based data modeling language. You write data models in readable Markdown, and this parser converts them into a machine-processable AST with full validation.
|
|
6
10
|
|
|
@@ -107,6 +111,7 @@ npx m3l validate ./models --strict --format json
|
|
|
107
111
|
| `## Name ::enum` | Enum definition |
|
|
108
112
|
| `## Name ::interface` | Interface definition |
|
|
109
113
|
| `## Name ::view` | Derived view |
|
|
114
|
+
| `## @name ::attribute` | Attribute registry entry |
|
|
110
115
|
| `- field: type` | Field definition |
|
|
111
116
|
| `- field: type?` | Nullable field |
|
|
112
117
|
| `- field: type[]` | Array field |
|
|
@@ -133,6 +138,7 @@ interface M3LAST {
|
|
|
133
138
|
enums: EnumNode[];
|
|
134
139
|
interfaces: ModelNode[];
|
|
135
140
|
views: ModelNode[];
|
|
141
|
+
attributeRegistry: AttributeRegistryEntry[];
|
|
136
142
|
errors: Diagnostic[];
|
|
137
143
|
warnings: Diagnostic[];
|
|
138
144
|
}
|
|
@@ -146,10 +152,10 @@ interface FieldNode {
|
|
|
146
152
|
label?: string;
|
|
147
153
|
type?: string;
|
|
148
154
|
params?: (string | number)[];
|
|
149
|
-
generic_params?: string[]; // map<K,V>
|
|
155
|
+
generic_params?: string[]; // map<K,V> -> ["K", "V"]
|
|
150
156
|
nullable: boolean;
|
|
151
157
|
array: boolean;
|
|
152
|
-
arrayItemNullable: boolean; // string?[]
|
|
158
|
+
arrayItemNullable: boolean; // string?[] -> true
|
|
153
159
|
kind: 'stored' | 'computed' | 'lookup' | 'rollup';
|
|
154
160
|
default_value?: string;
|
|
155
161
|
description?: string;
|
|
@@ -163,17 +169,61 @@ interface FieldNode {
|
|
|
163
169
|
loc: SourceLocation;
|
|
164
170
|
}
|
|
165
171
|
|
|
166
|
-
interface
|
|
172
|
+
interface FieldAttribute {
|
|
173
|
+
name: string;
|
|
174
|
+
args?: (string | number | boolean)[];
|
|
175
|
+
cascade?: string;
|
|
176
|
+
isStandard?: boolean; // true for M3L standard attributes
|
|
177
|
+
isRegistered?: boolean; // true for attributes in the registry
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface CustomAttribute {
|
|
181
|
+
content: string; // e.g. "MaxLength(100)"
|
|
182
|
+
raw: string; // e.g. "[MaxLength(100)]"
|
|
183
|
+
parsed?: { // structured parse result
|
|
184
|
+
name: string;
|
|
185
|
+
arguments: (string | number | boolean)[];
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface AttributeRegistryEntry {
|
|
167
190
|
name: string;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
191
|
+
description?: string;
|
|
192
|
+
target: ('field' | 'model')[];
|
|
193
|
+
type: string;
|
|
194
|
+
range?: [number, number];
|
|
195
|
+
required: boolean;
|
|
196
|
+
defaultValue?: string | number | boolean;
|
|
174
197
|
}
|
|
175
198
|
```
|
|
176
199
|
|
|
200
|
+
## Attribute Registry
|
|
201
|
+
|
|
202
|
+
Define custom attributes with validation metadata using `::attribute`:
|
|
203
|
+
|
|
204
|
+
```markdown
|
|
205
|
+
## @pii ::attribute
|
|
206
|
+
> Personal identifiable information marker
|
|
207
|
+
- target: [field]
|
|
208
|
+
- type: boolean
|
|
209
|
+
- default: false
|
|
210
|
+
|
|
211
|
+
## @audit_level ::attribute
|
|
212
|
+
> Audit compliance level
|
|
213
|
+
- target: [field, model]
|
|
214
|
+
- type: integer
|
|
215
|
+
- range: [1, 5]
|
|
216
|
+
- default: 1
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Attributes are classified into 3 tiers:
|
|
220
|
+
|
|
221
|
+
| Tier | `isStandard` | `isRegistered` | Example |
|
|
222
|
+
|------|-------------|----------------|---------|
|
|
223
|
+
| Standard | `true` | — | `@primary`, `@unique`, `@reference` |
|
|
224
|
+
| Registered | — | `true` | `@pii`, `@audit_level` (defined via `::attribute`) |
|
|
225
|
+
| Unregistered | — | — | `@some_unknown_attr` |
|
|
226
|
+
|
|
177
227
|
## Validation
|
|
178
228
|
|
|
179
229
|
The validator checks for semantic errors and style warnings:
|
|
@@ -184,7 +234,9 @@ The validator checks for semantic errors and style warnings:
|
|
|
184
234
|
| M3L-E001 | Rollup FK field missing `@reference` |
|
|
185
235
|
| M3L-E002 | Lookup FK field missing `@reference` |
|
|
186
236
|
| M3L-E004 | View references non-existent model |
|
|
237
|
+
| M3L-E005 | Duplicate model/enum name |
|
|
187
238
|
| M3L-E006 | Duplicate field name within model |
|
|
239
|
+
| M3L-E007 | Unresolved parent in inheritance |
|
|
188
240
|
|
|
189
241
|
**Warnings (--strict):**
|
|
190
242
|
| Code | Description |
|
|
@@ -221,6 +273,10 @@ Parse an M3L content string into an AST.
|
|
|
221
273
|
|
|
222
274
|
Parse and validate M3L files. Options: `{ strict?: boolean }`.
|
|
223
275
|
|
|
276
|
+
### `STANDARD_ATTRIBUTES: Set<string>`
|
|
277
|
+
|
|
278
|
+
The set of 27 M3L standard attribute names (e.g. `primary`, `unique`, `reference`, `computed`, ...).
|
|
279
|
+
|
|
224
280
|
### Lower-level API
|
|
225
281
|
|
|
226
282
|
```typescript
|
package/dist/lexer.js
CHANGED
|
@@ -3,7 +3,7 @@ const RE_H1 = /^# (.+)$/;
|
|
|
3
3
|
const RE_H2 = /^## (.+)$/;
|
|
4
4
|
const RE_H3 = /^### (.+)$/;
|
|
5
5
|
const RE_HR = /^-{3,}$/;
|
|
6
|
-
const RE_BLOCKQUOTE =
|
|
6
|
+
const RE_BLOCKQUOTE = /^\s*> (.+)$/;
|
|
7
7
|
const RE_LIST_ITEM = /^(\s*)- (.+)$/;
|
|
8
8
|
const RE_BLANK = /^\s*$/;
|
|
9
9
|
// H2 sub-patterns
|
|
@@ -143,6 +143,14 @@ function tokenizeH2(content, raw, line) {
|
|
|
143
143
|
const rest = typeMatch[3]?.trim() || '';
|
|
144
144
|
const { name, label } = parseNameLabel(namepart);
|
|
145
145
|
const data = { name, label };
|
|
146
|
+
// Parse inheritance: ::enum : Base1, Base2
|
|
147
|
+
const inheritMatch = rest.match(/^:\s*(.+?)(?:\s+@|\s*"|\s*$)/);
|
|
148
|
+
if (inheritMatch) {
|
|
149
|
+
data.inherits = inheritMatch[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
data.inherits = [];
|
|
153
|
+
}
|
|
146
154
|
if (typeIndicator === 'view') {
|
|
147
155
|
data.materialized = rest.includes('@materialized');
|
|
148
156
|
}
|
package/dist/parser.js
CHANGED
|
@@ -139,6 +139,7 @@ function handleEnumStart(token, state) {
|
|
|
139
139
|
type: 'enum',
|
|
140
140
|
source: state.file,
|
|
141
141
|
line: token.line,
|
|
142
|
+
inherits: data.inherits || [],
|
|
142
143
|
description: data.description,
|
|
143
144
|
values: [],
|
|
144
145
|
loc: { file: state.file, line: token.line, col: 1 },
|
|
@@ -265,11 +266,12 @@ function handleDirective(data, model, token, state) {
|
|
|
265
266
|
if (!attrs || attrs.length === 0)
|
|
266
267
|
return;
|
|
267
268
|
const attr = attrs[0];
|
|
268
|
-
if (attr.name === 'index') {
|
|
269
|
+
if (attr.name === 'index' || attr.name === 'unique') {
|
|
269
270
|
model.sections.indexes.push({
|
|
270
271
|
type: 'directive',
|
|
271
272
|
raw: data.raw_content,
|
|
272
273
|
args: attr.args,
|
|
274
|
+
unique: attr.name === 'unique',
|
|
273
275
|
loc: { file: state.file, line: token.line, col: 1 },
|
|
274
276
|
});
|
|
275
277
|
}
|
|
@@ -358,6 +360,7 @@ function handleSectionItem(data, model, token, state) {
|
|
|
358
360
|
raw: token.raw.trim().replace(/^- /, ''),
|
|
359
361
|
loc,
|
|
360
362
|
});
|
|
363
|
+
state.lastField = { name: token.raw.trim().replace(/^- /, '') };
|
|
361
364
|
return;
|
|
362
365
|
}
|
|
363
366
|
// Metadata section
|
|
@@ -420,6 +423,16 @@ function handleNestedItem(token, state) {
|
|
|
420
423
|
}
|
|
421
424
|
return;
|
|
422
425
|
}
|
|
426
|
+
// Nested items under relation in Relations section
|
|
427
|
+
if (state.currentSection === 'Relations' && state.lastField) {
|
|
428
|
+
const lastRelation = model.sections.relations[model.sections.relations.length - 1];
|
|
429
|
+
if (lastRelation && typeof lastRelation === 'object') {
|
|
430
|
+
if (key) {
|
|
431
|
+
lastRelation[key] = parseNestedValue(value || '');
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
423
436
|
// Nested items under a field
|
|
424
437
|
if (state.lastField) {
|
|
425
438
|
const field = state.lastField;
|
|
@@ -500,6 +513,16 @@ function handleBlockquote(token, state) {
|
|
|
500
513
|
if (!state.currentElement)
|
|
501
514
|
return;
|
|
502
515
|
const text = token.data.text;
|
|
516
|
+
// Field-level blockquote: apply to lastField if available (not for enums)
|
|
517
|
+
if (state.lastField && !isEnumNode(state.currentElement)) {
|
|
518
|
+
if (state.lastField.description) {
|
|
519
|
+
state.lastField.description += '\n' + text;
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
state.lastField.description = text;
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
503
526
|
if (state.currentElement.description) {
|
|
504
527
|
state.currentElement.description += '\n' + text;
|
|
505
528
|
}
|
|
@@ -667,6 +690,10 @@ function buildFieldNode(data, token, state) {
|
|
|
667
690
|
field.computed.platform = parts.platform;
|
|
668
691
|
}
|
|
669
692
|
}
|
|
693
|
+
// Inline comment as field description
|
|
694
|
+
if (!field.description && data.comment) {
|
|
695
|
+
field.description = data.comment;
|
|
696
|
+
}
|
|
670
697
|
return field;
|
|
671
698
|
}
|
|
672
699
|
/**
|
package/dist/resolver.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ParsedFile, M3LAST, ProjectInfo } from './types.js';
|
|
|
2
2
|
/** AST schema version — bump major on breaking structure changes */
|
|
3
3
|
export declare const AST_VERSION = "1.0";
|
|
4
4
|
/** Parser package version — kept in sync with package.json */
|
|
5
|
-
export declare const PARSER_VERSION = "0.1.
|
|
5
|
+
export declare const PARSER_VERSION = "0.1.4";
|
|
6
6
|
/**
|
|
7
7
|
* Resolve and merge multiple parsed file ASTs into a single M3LAST.
|
|
8
8
|
* Handles: inheritance resolution, duplicate detection, reference validation.
|
package/dist/resolver.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** AST schema version — bump major on breaking structure changes */
|
|
2
2
|
export const AST_VERSION = '1.0';
|
|
3
3
|
/** Parser package version — kept in sync with package.json */
|
|
4
|
-
export const PARSER_VERSION = '0.1.
|
|
4
|
+
export const PARSER_VERSION = '0.1.4';
|
|
5
5
|
/**
|
|
6
6
|
* Resolve and merge multiple parsed file ASTs into a single M3LAST.
|
|
7
7
|
* Handles: inheritance resolution, duplicate detection, reference validation.
|
package/dist/types.d.ts
CHANGED