rebase generation

This commit is contained in:
MaKarin
2026-04-07 19:40:41 +03:00
parent 73ddb1a948
commit aab7bfa691
180 changed files with 15512 additions and 364 deletions

View File

@@ -0,0 +1,389 @@
import { readdirSync, readFileSync } from 'node:fs';
import path from 'node:path';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function stripInlineComment(line) {
let inString = false;
let result = '';
for (let index = 0; index < line.length; index += 1) {
const current = line[index];
const next = line[index + 1];
if (current === '"' && line[index - 1] !== '\\') {
inString = !inString;
result += current;
continue;
}
if (!inString && current === '/' && next === '/') {
break;
}
result += current;
}
return result.trim();
}
// ---------------------------------------------------------------------------
// File discovery
// ---------------------------------------------------------------------------
export function getApiDslFiles(rootDir) {
const domainDir = path.join(rootDir, 'domain');
try {
return readdirSync(domainDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.api.dsl'))
.map((entry) => path.join(domainDir, entry.name))
.sort((left, right) => left.localeCompare(right));
} catch {
return [];
}
}
// ---------------------------------------------------------------------------
// Parser
//
// Parses all *.api.dsl files using a stack-based approach.
// This is the single canonical parser for the API DSL.
//
// Supported top-level blocks:
// enum <Name> { description?; value <Name> { label?; } }
// dto DTO.<Name> { description?; attribute <name> { ... } }
// api API.<Name> { description?; endpoint <name> { ... } }
//
// DTO attribute modifiers (any order inside the attribute block):
// type <type>;
// description "...";
// map Entity.field;
// sync Entity.field; (alias for map — used for computed/aggregate fields)
// is required;
// is nullable;
// is unique;
// key primary;
// label "...";
//
// Endpoint modifiers:
// label "METHOD /path";
// description "...";
// attribute <name> { type <type>; description?; }
//
// Returns:
// {
// files: string[],
// enums: EnumBlock[],
// dtos: DtoBlock[],
// apis: ApiBlock[],
// }
//
// EnumBlock = { name, description, values: EnumValue[] }
// EnumValue = { name, label }
// DtoBlock = { name, description, fields: DtoField[] }
// DtoField = { name, type, required, nullable, unique, primary, description, map, label }
// ApiBlock = { name, description, endpoints: Endpoint[] }
// Endpoint = { name, label, method, path, description, attributes: EndpointAttr[] }
// EndpointAttr = { name, type, description }
// ---------------------------------------------------------------------------
export function parseApiDsl(rootDir) {
const files = getApiDslFiles(rootDir);
const enums = [];
const dtos = [];
const apis = [];
const stack = [];
for (const filePath of files) {
const content = readFileSync(filePath, 'utf8');
const lines = content.split(/\r?\n/);
for (const rawLine of lines) {
const line = stripInlineComment(rawLine);
if (!line) continue;
const top = stack.at(-1);
// ── Top-level: enum block ──────────────────────────────────────────
const enumMatch = line.match(/^enum\s+([A-Za-z][A-Za-z0-9_]*)\s*\{$/);
if (!top && enumMatch) {
const enumBlock = { name: enumMatch[1], description: null, values: [] };
enums.push(enumBlock);
stack.push({ type: 'enum', ref: enumBlock });
continue;
}
// ── Top-level: dto block ───────────────────────────────────────────
const dtoMatch = line.match(/^dto\s+(DTO\.\w+)\s*\{$/);
if (!top && dtoMatch) {
const dto = { name: dtoMatch[1], description: null, fields: [] };
dtos.push(dto);
stack.push({ type: 'dto', ref: dto });
continue;
}
// ── Top-level: api block ───────────────────────────────────────────
const apiMatch = line.match(/^api\s+(API\.\w+)\s*\{$/);
if (!top && apiMatch) {
const api = { name: apiMatch[1], description: null, endpoints: [] };
apis.push(api);
stack.push({ type: 'api', ref: api });
continue;
}
// ── Inside enum ───────────────────────────────────────────────────
if (top?.type === 'enum') {
const descMatch = line.match(/^description\s+"(.*)"\s*;$/);
if (descMatch) {
top.ref.description = descMatch[1];
continue;
}
const valueMatch = line.match(/^value\s+([^\s{]+)\s*\{$/);
if (valueMatch) {
const enumValue = { name: valueMatch[1], label: null };
top.ref.values.push(enumValue);
stack.push({ type: 'enumValue', ref: enumValue });
continue;
}
}
// ── Inside enum value ─────────────────────────────────────────────
if (top?.type === 'enumValue') {
const labelMatch = line.match(/^label\s+"(.*)"\s*;$/);
if (labelMatch) {
top.ref.label = labelMatch[1];
continue;
}
}
// ── Inside dto ────────────────────────────────────────────────────
if (top?.type === 'dto') {
const descMatch = line.match(/^description\s+"(.*)"\s*;$/);
if (descMatch) {
top.ref.description = descMatch[1];
continue;
}
const attrMatch = line.match(/^attribute\s+(\w+)\s*\{$/);
if (attrMatch) {
const field = {
name: attrMatch[1],
type: null,
required: false,
nullable: false,
unique: false,
primary: false,
description: null,
map: null,
label: null,
};
top.ref.fields.push(field);
stack.push({ type: 'dtoField', ref: field });
continue;
}
}
// ── Inside dto attribute field ────────────────────────────────────
if (top?.type === 'dtoField') {
const typeMatch = line.match(/^type\s+(.+?)\s*;$/);
if (typeMatch) {
top.ref.type = typeMatch[1];
continue;
}
if (/^is\s+required\s*;$/.test(line)) {
top.ref.required = true;
continue;
}
if (/^is\s+nullable\s*;$/.test(line)) {
top.ref.nullable = true;
continue;
}
if (/^is\s+unique\s*;$/.test(line)) {
top.ref.unique = true;
continue;
}
if (/^key\s+primary\s*;$/.test(line)) {
top.ref.primary = true;
continue;
}
const descMatch = line.match(/^description\s+"(.*)"\s*;$/);
if (descMatch) {
top.ref.description = descMatch[1];
continue;
}
// map Entity.field; — canonical field mapping
const mapMatch = line.match(/^map\s+(\w+)\.(\w+)\s*;$/);
if (mapMatch) {
top.ref.map = `${mapMatch[1]}.${mapMatch[2]}`;
continue;
}
// sync Entity.field; — aggregate / computed field mapping (treated as map)
const syncMatch = line.match(/^sync\s+(\w+)\.(\w+)\s*;$/);
if (syncMatch) {
top.ref.map = `${syncMatch[1]}.${syncMatch[2]}`;
top.ref.sync = true;
continue;
}
const labelMatch = line.match(/^label\s+"(.*)"\s*;$/);
if (labelMatch) {
top.ref.label = labelMatch[1];
continue;
}
}
// ── Inside api ────────────────────────────────────────────────────
if (top?.type === 'api') {
const descMatch = line.match(/^description\s+"(.*)"\s*;$/);
if (descMatch) {
top.ref.description = descMatch[1];
continue;
}
const epMatch = line.match(/^endpoint\s+(\w+)\s*\{$/);
if (epMatch) {
const ep = {
name: epMatch[1],
label: null,
method: null,
path: null,
description: null,
attributes: [],
};
top.ref.endpoints.push(ep);
stack.push({ type: 'endpoint', ref: ep });
continue;
}
}
// ── Inside endpoint ───────────────────────────────────────────────
if (top?.type === 'endpoint') {
const labelMatch = line.match(/^label\s+"([^"]+)"\s*;$/);
if (labelMatch) {
top.ref.label = labelMatch[1];
const parts = labelMatch[1].split(' ', 2);
top.ref.method = parts[0]?.toUpperCase() ?? null;
top.ref.path = parts[1] ?? null;
continue;
}
const descMatch = line.match(/^description\s+"(.*)"\s*;$/);
if (descMatch) {
top.ref.description = descMatch[1];
continue;
}
const attrMatch = line.match(/^attribute\s+(\w+)\s*\{$/);
if (attrMatch) {
const attr = { name: attrMatch[1], type: null, description: null };
top.ref.attributes.push(attr);
stack.push({ type: 'endpointAttr', ref: attr });
continue;
}
}
// ── Inside endpoint attribute ─────────────────────────────────────
if (top?.type === 'endpointAttr') {
const typeMatch = line.match(/^type\s+(.+?)\s*;$/);
if (typeMatch) {
top.ref.type = typeMatch[1];
continue;
}
const descMatch = line.match(/^description\s+"(.*)"\s*;$/);
if (descMatch) {
top.ref.description = descMatch[1];
continue;
}
}
// ── Closing brace — pop the stack ─────────────────────────────────
if (/^}\s*;?$/.test(line)) {
stack.pop();
}
}
}
return { files, enums, dtos, apis };
}
// ---------------------------------------------------------------------------
// Summary builder
//
// Produces the serialisable api-summary.json object.
// ---------------------------------------------------------------------------
export function buildApiSummary(rootDir) {
const { files, enums, dtos, apis } = parseApiDsl(rootDir);
// Detect duplicate DTO names
const dtoNames = new Set();
for (const dto of dtos) {
if (dtoNames.has(dto.name)) {
throw new Error(`Duplicate DTO definition: ${dto.name}`);
}
dtoNames.add(dto.name);
}
// Detect duplicate API names
const apiNames = new Set();
for (const api of apis) {
if (apiNames.has(api.name)) {
throw new Error(`Duplicate API definition: ${api.name}`);
}
apiNames.add(api.name);
}
return {
sourceFiles: files.map((filePath) =>
path.relative(rootDir, filePath).replaceAll('\\', '/'),
),
enums: enums.map((e) => ({
name: e.name,
description: e.description,
values: e.values.map((v) => ({ name: v.name, label: v.label })),
})),
dtos: dtos.map((dto) => ({
name: dto.name,
description: dto.description,
fields: dto.fields.map((field) => ({
name: field.name,
type: field.type,
required: field.required,
nullable: field.nullable,
unique: field.unique,
primary: field.primary,
description: field.description,
map: field.map,
sync: field.sync ?? false,
label: field.label,
})),
})),
apis: apis.map((api) => ({
name: api.name,
description: api.description,
endpoints: api.endpoints.map((ep) => ({
name: ep.name,
label: ep.label,
method: ep.method,
path: ep.path,
description: ep.description,
attributes: ep.attributes.map((attr) => ({
name: attr.name,
type: attr.type,
description: attr.description,
})),
})),
})),
};
}