Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/generator-support-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Generator support drift

# `reference/generator-support.md` carries a copy of generation's
# `FEATURES.md`: the matrix its conformance suite proves. This checks the copy
# is current. Like the IDL drift check it needs no Rust build, only a fetch,
# so it runs per pull request. It's kept separate from the Docs workflow,
# because a fetch from another repo shouldn't be able to block a deploy.

on:
schedule:
# A generator gaining a feature never touches this repo, so a
# push-triggered run would not see it. Weekly catches that side.
- cron: '45 7 * * 1'
pull_request:
paths:
- 'docs/docs/reference/generator-support.md'
- 'scripts/sync-generator-support.py'
- '.github/workflows/generator-support-drift.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: generator-support-drift-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v7
with:
python-version: "3.14"

- name: Check for drift
shell: bash
run: |
if ! python3 scripts/sync-generator-support.py --check | tee drift.txt; then
{
echo '### Generator support drift'
echo
echo '```'
cat drift.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
4 changes: 3 additions & 1 deletion docs/docs/guide/idl/validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
put on a value of a type it doesn't apply to, a keyword argument that isn't
a property or whose literal type is wrong, a missing argument for a property
without a default, or an undeclared `params.*` reference all fail the build. Generated **Rust** code then
[runs them](#in-generated-code). Generated TypeScript doesn't yet.
[runs them](#in-generated-code). Generated TypeScript doesn't yet: it
generates the types without the checks, and `comline generate` warns. See
[Generator support](../../reference/generator-support.md).

A **validator** is a named, parameterised check you attach to a
[struct](structure.md) or [error](error.md) field, or to a
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ parse → resolve imports → validate → (freeze into .comline/) → (generate
| `comline clean [--dry-run]` | Remove generated code (the `generate` output root or files). Never touches `.comline/`. |
| `comline reset [--force] [--dry-run]` | **Destructive.** Delete `.comline/` — the whole version history, no undo — *and* generated code. Next `build` starts over at `0.0.1`. On a terminal it shows how many versions will be lost and makes you type `reset`; without a terminal it refuses (exit `2`) unless `--force`. |
| `comline completions <shell>` | Print a completion script to stdout. `bash` `zsh` `fish` `powershell` `elvish`. |
| `comline targets` | Print one `language#version` per line to stdout — the token syntax `config.idp`'s `code_generation.languages` expects — for every generator this binary was compiled with. Ignores `--path`: it reflects the build, not a project. |
| `comline targets [--features [--json]]` | Print one `language#version` per line to stdout — the token syntax `config.idp`'s `code_generation.languages` expects — for every generator this binary was compiled with. Ignores `--path`: it reflects the build, not a project. `--features` also lists what each generator supports, feature by feature ([Generator support](generator-support.md)); `--json` prints that as JSON. |

`--out` / `--layout` / `--mode` override one target and need `--target` when
several are configured. `COMLINE_GENERATE_OUT` / `_LAYOUT` / `_MODE` do the same
Expand Down
75 changes: 75 additions & 0 deletions docs/docs/reference/generator-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Generator support

What each code generator implements, feature by feature. The table below is
the one the generators' conformance suite produces, so every entry in it has
been checked, not just claimed.

## What the states mean

| State | Generating a schema that uses the feature |
|---|---|
| ✅ **supported** | Works. |
| 🟡 **partial** | Works, with the limit the note states. |
| ⚪ **ignored** | Succeeds, but the output is what it would be without the feature: the types are right, the feature has no effect. `comline generate` warns, and so does the editor. |
| ❌ **unsupported** | Is refused, with an error naming every use. The editor warns at each use. |

## How a claim is proven

Each generator states its support in a `SUPPORT` table in its own crate. The
conformance suite in
[`ComlineProject/generation`](https://github.com/ComlineProject/generation)
runs every generator over a corpus of schemas and checks each claim against
what the generator actually does:

- **supported** or **partial**: a schema in the corpus uses the feature,
generates, and matches its checked-in output. Where the feature can be taken
out of the schema, the output must change without it; otherwise the feature
is really being ignored.
- **ignored**: the output is identical with the feature taken out, apart from
the IR hash, which names the schema.
- **unsupported**: every schema using the feature is refused, with an error
naming the feature.

A claim that no schema in the corpus can back fails the suite. So a new
feature, or a generator that gains one, comes with a schema that proves it.
The proving schemas are named in each cell below.

## The matrix

<!-- features:start -->
<!-- Copied from ComlineProject/generation's FEATURES.md by scripts/sync-generator-support.py - don't edit by hand. -->

| Feature | rust 1.70.0 | typescript 5.0 |
|---|---|---|
| structs <br><sub>`structs`</sub> | ✅ supported <br><sub>proven by `primitives`, `optional_and_arrays`, `type_refs` +8 more</sub> | ✅ supported <br><sub>proven by `primitives`, `optional_and_arrays`, `type_refs` +10 more</sub> |
| enums <br><sub>`enums`</sub> | ✅ supported <br><sub>proven by `enum_basic`</sub> | ✅ supported <br><sub>proven by `enum_basic`</sub> |
| optional fields <br><sub>`optional-fields`</sub> | ✅ supported <br><sub>proven by `optional_and_arrays`, `type_refs`, `shapes` +3 more</sub> | ✅ supported <br><sub>proven by `optional_and_arrays`, `type_refs`, `shapes` +4 more</sub> |
| arrays <br><sub>`arrays`</sub> | ✅ supported <br><sub>proven by `optional_and_arrays`, `type_refs`, `shapes` +3 more</sub> | ✅ supported <br><sub>proven by `optional_and_arrays`, `type_refs`, `shapes` +4 more</sub> |
| fixed-length arrays <br><sub>`fixed-arrays`</sub> | ✅ supported <br><sub>proven by `shapes`</sub> | 🟡 partial: rendered as `T[]`: the length isn't part of the type <br><sub>proven by `shapes`, `large_fixed_arrays_nested`</sub> |
| fixed arrays over 32 elements, nested <br><sub>`large-fixed-arrays-nested`</sub> | ❌ unsupported: a fixed array over 32 elements renders only as a struct or error field's own, non-optional type (serde's derive stops at 32) <br><sub>proven by `large_fixed_arrays_nested`</sub> | 🟡 partial: rendered as `T[]`: the length isn't part of the type <br><sub>proven by `large_fixed_arrays_nested`</sub> |
| unions <br><sub>`unions`</sub> | ❌ unsupported: Rust has no anonymous sum type to render one as <br><sub>proven by `unions`</sub> | ✅ supported <br><sub>proven by `unions`</sub> |
| type aliases <br><sub>`type-aliases`</sub> | ✅ supported <br><sub>proven by `validators`, `validators_on_decode`</sub> | ✅ supported <br><sub>proven by `validators`, `validators_on_decode`, `unions`</sub> |
| constants <br><sub>`constants`</sub> | ✅ supported <br><sub>proven by `constant`, `shapes`, `validators` +1 more</sub> | ✅ supported <br><sub>proven by `constant`, `shapes`, `validators` +1 more</sub> |
| protocols <br><sub>`protocols`</sub> | ✅ supported <br><sub>proven by `protocol`, `error_shapes`, `validators` +2 more</sub> | ✅ supported <br><sub>proven by `protocol`, `error_shapes`, `validators` +2 more</sub> |
| errors <br><sub>`errors`</sub> | ✅ supported <br><sub>proven by `protocol`, `error_shapes`</sub> | ✅ supported <br><sub>proven by `protocol`, `error_shapes`</sub> |
| functions that throw <br><sub>`throws`</sub> | ✅ supported <br><sub>proven by `protocol`, `error_shapes`</sub> | ✅ supported <br><sub>proven by `protocol`, `error_shapes`</sub> |
| validators <br><sub>`validators`</sub> | ✅ supported <br><sub>proven by `validators`, `validators_on_decode`</sub> | ⚪ ignored: the types are generated; the checks aren't run yet ([core#96](https://github.com/ComlineProject/core/issues/96)) <br><sub>proven by `validators`, `validators_on_decode`</sub> |
| imports between schemas <br><sub>`imports`</sub> | ✅ supported <br><sub>proven by `cross_schema`, `import_alias`, `import_glob` +4 more</sub> | ✅ supported <br><sub>proven by `cross_schema`, `import_alias`, `import_glob` +4 more</sub> |
| nested namespaces <br><sub>`nested-namespaces`</sub> | ✅ supported <br><sub>proven by `nested_namespace`, `external_std`, `validators` +1 more</sub> | ✅ supported <br><sub>proven by `nested_namespace`, `external_std`, `validators` +1 more</sub> |
| `lib` mode <br><sub>`lib-mode`</sub> | ✅ supported <br><sub>proven by `nested_namespace`, `external_std`</sub> | ✅ supported <br><sub>proven by `nested_namespace`, `external_std`</sub> |
| `external_std` <br><sub>`external-std`</sub> | ✅ supported <br><sub>proven by `external_std`</sub> | ✅ supported <br><sub>proven by `external_std`</sub> |
| `validate_on_decode` <br><sub>`validate-on-decode`</sub> | ✅ supported <br><sub>proven by `validators_on_decode`</sub> | ⚪ ignored: no checks are generated to run on decode yet ([core#96](https://github.com/ComlineProject/core/issues/96)) <br><sub>proven by `validators_on_decode`</sub> |
| JSON-RPC framing <br><sub>`framing-jsonrpc`</sub> | ✅ supported <br><sub>proven by `framing_jsonrpc`</sub> | ✅ supported <br><sub>proven by `framing_jsonrpc`</sub> |

<!-- features:end -->

## Elsewhere

- `comline targets --features` prints this for the generators your `comline`
was built with, and `--json` prints it as JSON (see the [CLI](cli.md)).
- In the editor, a schema using a feature a language declared in `config.idp`
can't generate gets a warning at the use. Hovering the language in
`config.idp` shows what its generator supports.
- Each generator's repository README carries its own column:
[comline-rust](https://github.com/ComlineProject/comline-rust#features),
[comline-typescript](https://github.com/ComlineProject/comline-typescript#features).
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ nav:
- Tools:
- Tools: 'reference/tools/index.md'
- CLI: 'reference/cli.md'
- Generator support: 'reference/generator-support.md'


- Design:
Expand Down
96 changes: 96 additions & 0 deletions scripts/sync-generator-support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Keep the generator support page in step with what conformance proved.

`ComlineProject/generation`'s conformance suite checks every generator's
feature claims against its corpus and writes the result to `FEATURES.md` at
the repository root. This copies that file's matrix into
`docs/docs/reference/generator-support.md`, between its
`<!-- features:start -->` / `<!-- features:end -->` markers, so the page says
exactly what was proven. The issue references in it (`core#96`) become links.

python3 scripts/sync-generator-support.py # rewrite the page
python3 scripts/sync-generator-support.py --check # exit 1 on drift
python3 scripts/sync-generator-support.py --file ../generation/FEATURES.md

Stdlib only: no Rust toolchain needed, so CI runs it per pull request.
"""

from __future__ import annotations

import argparse
import re
import sys
import urllib.error
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
PAGE = ROOT / "docs/docs/reference/generator-support.md"
RAW = "https://raw.githubusercontent.com/ComlineProject/generation/{rev}/FEATURES.md"
START = "<!-- features:start -->"
END = "<!-- features:end -->"

# Repositories an issue reference may name; anything else is left as text
# (`rust#1.70.0` is a target, not an issue).
REPOS = ("core", "generation", "cli", "comline-rust", "comline-typescript", "language-server", "runtime", "docs")
ISSUE_RE = re.compile(r"\b(" + "|".join(re.escape(r) for r in REPOS) + r")#(\d+)\b(?![\d.])")


def read_features(file: Path | None, rev: str) -> str:
if file:
return file.read_text(encoding="utf-8")
url = RAW.format(rev=rev)
try:
with urllib.request.urlopen(url, timeout=30) as response:
return response.read().decode("utf-8")
except urllib.error.HTTPError as error:
sys.exit(f"error: fetching {url} failed ({error.code} {error.reason})")
except urllib.error.URLError as error:
sys.exit(f"error: fetching {url} failed ({error.reason})")


def section(features: str) -> str:
"""The marked section the page should hold: the matrix, issues linked."""
table = [line for line in features.splitlines() if line.startswith("|")]
if not table:
sys.exit("error: FEATURES.md has no table")
linked = "\n".join(
ISSUE_RE.sub(lambda m: f"[{m[0]}](https://github.com/ComlineProject/{m[1]}/issues/{m[2]})", line)
for line in table
)
return (
f"{START}\n"
"<!-- Copied from ComlineProject/generation's FEATURES.md by "
"scripts/sync-generator-support.py - don't edit by hand. -->\n\n"
f"{linked}\n\n{END}"
)


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--check", action="store_true", help="exit 1 if the page is out of date")
parser.add_argument("--file", type=Path, help="read FEATURES.md from here instead of fetching it")
parser.add_argument("--rev", default="master", help="generation revision to fetch (default: master)")
args = parser.parse_args()

page = PAGE.read_text(encoding="utf-8")
start, end = page.find(START), page.find(END)
if start < 0 or end < 0:
sys.exit(f"error: {PAGE.relative_to(ROOT)} has no {START} ... {END} section")
current = page[start : end + len(END)]
wanted = section(read_features(args.file, args.rev))

if current == wanted:
print("generator support page is up to date")
return 0
if args.check:
print(f"{PAGE.relative_to(ROOT)} is out of date with generation's FEATURES.md.")
print("Run: python3 scripts/sync-generator-support.py")
return 1
PAGE.write_text(page[:start] + wanted + page[end + len(END) :], encoding="utf-8")
print(f"updated {PAGE.relative_to(ROOT)}")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading