diff --git a/README.md b/README.md index 10a7931..febcd74 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,138 @@ # AL2DBML + Generates a DBML database schema from a Business Central AL project by parsing table and field definitions. + +## Prerequisites + +No runtime required — AL2DBML is distributed as a self-contained binary. + +| Platform | Supported | +|---|---| +| Windows (x64) | ✓ | +| macOS (Apple Silicon) | ✓ | +| Linux (x64) | ✓ | + +## Installation + +### Windows (PowerShell) + +```powershell +$dest = "$env:LOCALAPPDATA\al2dbml" +New-Item -ItemType Directory -Force -Path $dest | Out-Null +Invoke-WebRequest -Uri "https://github.com/OGR-67/AL2DBML/releases/latest/download/al2dbml-win-x64.exe" -OutFile "$dest\al2dbml.exe" +$path = [Environment]::GetEnvironmentVariable("Path", "User") +if ($path -notlike "*al2dbml*") { + [Environment]::SetEnvironmentVariable("Path", "$path;$dest", "User") +} +Write-Host "Done. Restart your terminal." +``` + +### macOS + +```bash +curl -L https://github.com/OGR-67/AL2DBML/releases/latest/download/al2dbml-osx-arm64 -o al2dbml +chmod +x al2dbml +xattr -d com.apple.quarantine al2dbml +sudo mv al2dbml /usr/local/bin/al2dbml +``` + +> **Note:** The `xattr` command removes the Gatekeeper quarantine flag that macOS sets on downloaded binaries. Without it, macOS will block the app on first run. If you downloaded the binary manually instead of using this script, go to **System Settings → Privacy & Security** and click **Allow Anyway**. + +### Linux + +```bash +curl -L https://github.com/OGR-67/AL2DBML/releases/latest/download/al2dbml-linux-x64 -o al2dbml +chmod +x al2dbml +sudo mv al2dbml /usr/local/bin/al2dbml +``` + +## Quick start + +```bash +# 1. Initialize AL2DBML in your project (run once) +al2dbml init + +# 2. Generate the DBML schema +al2dbml generate +``` + +## Commands + +### `generate` + +Parses AL files and generates a `.dbml` schema file. + +``` +al2dbml generate [-i ] [-o ] [-n ] +``` + +| Option | Description | Default | +|---|---|---| +| `-i`, `--input` | Path to an AL project folder, `.al` file, or `.code-workspace` file | Value from `config.local.json`, then `.` | +| `-o`, `--output` | Output directory | Value from `config.json`, then `.` | +| `-n`, `--name` | Output file name (without extension) | Value from `config.json`, then `schema` | + +### `init` + +Initializes AL2DBML interactively in the current directory. Creates the `.al2dbml/` config folder and optionally sets up a pre-commit hook. + +``` +al2dbml init +``` + +Re-running `init` on an existing config pre-fills the prompts with current values. + +## Configuration + +`init` creates two files in `.al2dbml/`: + +| File | Versioned | Content | +|---|---|---| +| `config.json` | Yes | Output path and file name — shared across contributors | +| `config.local.json` | No (gitignored) | Input path — contributor-specific, useful for workspaces | + +`config.json` example: +```json +{ + "Output": { + "Path": "./docs/", + "Name": "schema" + } +} +``` + +`config.local.json` example: +```json +{ + "Input": { + "Path": "./MyProject.code-workspace" + } +} +``` + +## Pre-commit hook + +When enabled during `init`, AL2DBML adds a section to `.git/hooks/pre-commit` that automatically regenerates the schema on each commit: + +```sh +# [al2dbml-start] +if command -v al2dbml > /dev/null 2>&1; then + al2dbml generate || printf "\033[33mWarning: al2dbml generate failed, skipping DBML update.\033[0m\n" +else + printf "\033[33mWarning: al2dbml not found, skipping DBML update.\033[0m\n" +fi +# [al2dbml-end] +``` + +If `al2dbml` is not installed on a contributor's machine, the hook prints a warning and lets the commit proceed normally. + +To remove the hook, run: + +```bash +al2dbml remove-hook +``` + +## Contributing + +- [Architecture](docs/ARCHITECTURE.md) — project structure, CLI design, DI setup +- [AL Parsing](docs/AL_PARSING.md) — AL syntax specifics and parsing decisions diff --git a/docs/AL_PARSING.md b/docs/AL_PARSING.md new file mode 100644 index 0000000..397574d --- /dev/null +++ b/docs/AL_PARSING.md @@ -0,0 +1,107 @@ +# AL File Parsing + +This document describes the AL parsing specifics relevant to this project. It does not cover the full AL language specification — only what is needed to generate DBML output. + +## Supported file types + +| AL type | Detection keyword | Handled by | +|---|---|---| +| Table | `table` | `ParseTable` | +| Table extension | `tableextension` | `ParseTableExtension` | +| Enum | `enum` | `ParseEnum` | +| Enum extension | `enumextension` | `ParseEnumExtension` | +| Other (page, report, codeunit...) | — | Silently skipped | + +## File type detection + +Detection is done by regex on the **file content**, not the filename or extension. The detection order matters: + +1. `enumextension` before `enum` — an extension file could partially match the enum pattern +2. `tableextension` before `table` — same reason + +## Object identifiers + +Every AL object has a numeric ID: `table 50100 "Customer"`. The ID is **ignored** — only the name is extracted and used in the DBML output. + +## Name conventions + +AL identifiers can be quoted or unquoted: +- `"My Field"` → quoted, typically used for names with spaces or special characters +- `MyField` → unquoted + +Names containing `/` must be re-quoted in DBML output (e.g. `"No./Name"`) since `/` is not valid in an unquoted DBML identifier. This is handled by `AlSyntaxHelper.CleanName`. + +## Table parsing + +### Fields + +AL field syntax: +```al +field(50; "My Field"; Text[100]) { ... } +``` + +The field body `{ ... }` is parsed for: +- `FieldClass = FlowField` → marks the column as a FlowField +- `CalcFormula = ...` → extracts the formula as a string + +### Primary keys + +Primary keys are detected from `key(...)` declarations: +- A key named `PK` is treated as the primary key +- A key with `Clustered = true` in its body is treated as the primary key +- Composite keys (comma-separated fields) are supported + +```al +key(PK; "No.") { Clustered = true; } +``` + +## Table extensions + +A `tableextension` extends an existing base table: +```al +tableextension 50100 "My Extension" extends "Customer" { ... } +``` + +Fields from the extension are **merged** into the base table's `DBMLTable`. If the base table has not been parsed yet, a new `DBMLTable` is created for it and will be enriched when the base table file is processed. Parse order across files therefore does not matter. + +## Enum parsing + +An enum can optionally implement one or more interfaces via the `implements` clause: + +```al +enum 50100 "My Status" implements "IStatus", "ILabel" +{ + value(0; Open) { Caption = 'Open'; } + value(1; Closed) { Caption = 'Closed'; } +} +``` + +The `implements` clause is handled by the detection regex but the interface names are **not captured** in the output — only the enum name and its values matter for DBML. The corresponding interface definition files (`.al` files containing `interface "IStatus" { ... }`) are `Unknown` type and silently skipped. + +### Detection order caveat + +The `Enum` detection pattern starts with `^\s*enum\s+`. Since `enumextension` also starts with `enum`, checking `Enum` before `EnumExtension` would misclassify extension files. The correct order is always `EnumExtension` → `Enum`. + +## Enum extensions + +```al +enumextension 50101 "My Status Ext" extends "My Status" { value(2; Pending) } +``` + +Values are merged into the base enum. Duplicate values are ignored. + +## FlowFields + +AL FlowFields are calculated fields with no physical storage: +```al +field(20; "Balance"; Decimal) { + FieldClass = FlowField; + CalcFormula = sum("Ledger Entry".Amount where("Account No." = field("No."))); +} +``` + +FlowFields are included in the DBML output as regular columns. The `CalcFormula` is captured and can be used as a note. DBML has no native equivalent for this concept. + +## State accumulation + +`IAlParser` accumulates parsed tables and enums in internal state across multiple `Parse*` calls. This allows multi-file parsing (folder, workspace) to produce a single unified `OutputSchema`. State is cleared between command executions via `ClearOutputSchema()`. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6e59c5a..ad35d5a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2,6 +2,8 @@ AL2DBML follows a Clean Architecture pattern: the CLI resolves dependencies through a dedicated Composition Root, while the Parser acts as infrastructure implementing contracts defined by the Application layer — keeping the Domain model free of any external dependency. +## Dependency graph + ```mermaid graph TD CLI["AL2DBML.CLI
Presentation"] @@ -24,3 +26,63 @@ graph TD TESTS --> CORE TESTS --> DI ``` + +## CLI layer + +The CLI is built on **Spectre.Console.Cli** and exposes two commands: + +| Command | Description | +|---|---| +| `generate` | Parses AL input and writes a `.dbml` file | +| `init` | Initializes the `.al2dbml/` config directory interactively | + +### Input strategy pattern + +`generate` delegates input handling to an `IInputStrategy` resolved by `InputStrategyFactory` based on the detected input type: + +```mermaid +graph TD + GC["GenerateCommand"] + F["InputStrategyFactory"] + SF["SingleFileInputStrategy"] + FO["FolderInputStrategy"] + WS["WorkspaceInputStrategy"] + + GC --> F + F --> SF + F --> FO + F --> WS + FO --> SF + WS --> FO +``` + +| Strategy | Input | Behaviour | +|---|---|---| +| `SingleFileInputStrategy` | `.al` file | Detects file type, calls the appropriate parser method | +| `FolderInputStrategy` | Directory | Scans for `*.al` recursively, delegates each file to `SingleFileInputStrategy` | +| `WorkspaceInputStrategy` | `.code-workspace` | Reads workspace JSON, resolves each folder path, delegates to `FolderInputStrategy` | + +Files with an unsupported type (`AlFileType.Unknown`) are silently skipped — this is intentional since AL projects contain many non-table/enum objects. + +### Configuration + +`init` creates a `.al2dbml/` directory at the project root with two files: + +| File | Tracked | Content | +|---|---|---| +| `config.json` | versioned | output path and file name (shared across contributors) | +| `config.local.json` | gitignored | input path (contributor-specific, especially for workspaces) | + +`generate` reads these files if present; CLI arguments always take precedence. + +### Services + +| Service | Scope | Responsibility | +|---|---|---| +| `FileSystemService` | static | Input type detection, directory scan | +| `ConfigService` | scoped | Read/write `.al2dbml/` config files | +| `ParsingTracker` | scoped | Track file count and elapsed time per command execution | + +### DI integration + +Spectre.Console.Cli is integrated with `Microsoft.Extensions.DependencyInjection` via a custom `TypeRegistrar`/`TypeResolver`. A new DI scope is created per command execution so scoped services (parser state, tracker) are properly isolated.