crabster

Contribuer

Ce document n'existe qu'en un seul exemplaire dans le dépôt : les deux langues du site pointent sur le même texte.

{% raw %}

🇫🇷 Ce document est en français ; un résumé en anglais suit chaque section clé. 🇬🇧 This document is in French, with an English summary after each key section.

Merci de votre intérêt. Crabster a terminé sa Phase 3 (plan de travail) : un modèle CDL génère un projet Rust qui compile, passe ses tests et sert une API CRUD complète, et crabster record ajoute un enregistrement à un projet existant. La Phase 4 — authentification et sécurité — est ouverte.

Par où commencer🔗

Avant toute contribution de code, lisez dans l'ordre :

  1. Vision et objectifs — les principes qui arbitrent les désaccords
  2. Architecture technique — les choix techniques et leurs alternatives écartées
  3. Plan de travail — la phase en cours et ce qui est hors périmètre

Une proposition qui contredit un principe de la vision ou un ADR sera discutée sur ce terrain-là. Ce n'est pas un refus de principe : les ADR peuvent être révisés, mais explicitement, par un nouvel ADR qui documente le changement — pas par accumulation de PR.

EN: Read the vision, architecture, and roadmap docs first. Proposals that contradict a documented principle or ADR get discussed on those terms. ADRs can be revised — explicitly, via a new ADR, not by accumulated PRs.

Prérequis🔗

Cela suffit pour ./check. Le palier --all demande davantage, et le tableau plus bas dit quel test réclame quoi et pourquoi : Docker, tsc, npm, Chrome, python3, kubectl, helm, et un minikube démarré avec son addon ingress pour le seul test qui déploie.

Boucle de développement🔗

./check          # format, lint, docs, tests. Moins d'une minute à chaud.
./check --all    # plus les projets générés, l'empaquetage et l'audit. Minutes.

./check --all avant chaque push, pas avant certains. C'est le seul garde-fou : le dépôt est privé, ses minutes GitHub Actions sont facturées et ne sont pas payées, donc .github/workflows/ci.yml ne se déclenche plus sur rien. Les jobs ne sont pas supprimés et ne sont pas cassés — seuls les déclencheurs push, pull_request et schedule ont été retirés, et le fichier dit comment les remettre.

Ce que ./check fait tourner, et pourquoi chaque étape existe :

ÉtapeCe qu'elle seule attrape
cargo fmt --check—
cargo clippy --workspace --all-targetsRUSTFLAGS: -D warnings est actif : un avertissement fait échouer
cargo doc --workspace --no-depsLes deux crates bibliothèque portent #![warn(missing_docs)], et c'est lancé sous RUSTDOCFLAGS: -D warnings — un item public non documenté échoue là et nulle part ailleurs
cargo test --workspace—
--all : cargo test -- --ignoredCompile des projets générés entiers et les exécute. C'est le seul endroit où les templates sont prouvés produire du Rust valide. Docker et tsc sont requis : quatre de ces tests démarrent un vrai PostgreSQL et un vrai MySQL et y font tourner les migrations générées, un autre compile le client TypeScript généré. Ils échouent plutôt que de se sauter — un test qui passe en ne s'exécutant pas est précisément la couverture que ce palier existe pour cesser de feindre. npm i -g typescript pour le second. Un troisième démarre Keycloak et rejoue en python3 tout le flux de connexion d'un navigateur. Un quatrième, un cinquième et un sixième construisent les blueprints React, Vue et Angular (npm) et rendent leur page dans Chrome en mode headless — un bundle qui se construit ne dit pas qu'une application démarre, ce qu'un polyfill manquant a prouvé sur Angular. Un septième génère un projet MongoDB et exécute sa propre suite contre une base qu'il démarre. Un huitième fait de même avec Meilisearch. Un neuvième rend une surcouche Kustomize avec kubectl, un dixième passe le chart au crible de helm — deux binaires à installer plutôt qu'à démarrer, là où les autres tirent leurs conteneurs. Le onzième est le seul du dépôt à demander quelque chose de déjà en route : il déploie un système entier sur minikube et le fait répondre par son Ingress, ce qui est la definition of done de la phase 17 exécutée plutôt que retenue. Il est donc le seul que ./check --all saute en le disant plutôt que d'échouer : Docker absent est une machine qui ne l'a pas, un cluster arrêté est un état qu'on démarre — et un cluster arrêté a bloqué un push dont le diff ne touchait pas Kubernetes. minikube start && minikube addons enable ingress et il tourne ; CRABSTER_K8S=1 le force sur le cluster que kubectl vise. Il retire son namespace même s'il explose au milieu, et prend une quarantaine de secondes une fois les trois images en cache — plusieurs minutes la première fois, puisqu'il les construit. À part lui, aucun ne demande qu'on lui fournisse quoi que ce soit : une suite générée démarre les serveurs dont elle a besoin, et TEST_DATABASE_URL ou TEST_SEARCH_ADDRESS ne sont là que pour lui en imposer un autre
--all : cargo package --workspaceReconstruit chaque crate depuis son propre tarball : un fichier hors du paquet ne se voit que là. Sauté si l'arbre n'est pas propre, cargo package lisant ce que git suit
--all : cargo auditSauté s'il n'est pas installé. Un avis est accepté, et un seul : RUSTSEC-2023-0071, la faille Marvin de rsa, moyenne et sans correctif. Elle arrive par sqlx-mysql, dont crabster introspect a besoin pour lire un schéma MySQL. Ce crate n'emploie rsa que dans connection/auth.rs, et seulement pour chiffrer un mot de passe avec la clé publique du serveur ; Marvin récupère une clé privée par le temps de déchiffrement. Ce client n'en détient aucune et ne déchiffre rien. Le raisonnement est écrit dans check, avec les deux commandes qui permettent de le revérifier plutôt que de le croire

./check --all efface target/e2e en terminant, et ce n'est pas une optimisation mais l'inverse : chaque passage suivant recompile des projets générés entiers depuis rien.

La raison est le disque. target/e2e est le cache partagé du palier lourd, rien n'y évince jamais rien — chaque projet généré y est un crate différent — et il grossit d'une vingtaine de gigaoctets par passage complet. Time Machine prend un instantané local toutes les heures, un passage complet dure plus longtemps que cela : un répertoire laissé debout est donc un répertoire qu'un instantané épingle, et l'effacer après coup ne libère alors plus rien. L'espace ne revient qu'à la purge des instantanés. C'est ainsi que cette machine a atteint 47 Go de cache, rempli son disque, et laissé le disque virtuel de Docker en lecture seule sans qu'aucun prune puisse le récupérer.

L'effacer dans l'heure est ce qui le tient hors des instantanés. Le coût est une exécution lente à chaque fois, payée sciemment.

CRABSTER_KEEP_E2E=1 ./check --all le conserve, pour une session passée à le lancer en boucle.

Ce que la machine ne peut pas remplacer : trois systèmes d'exploitation, et un cache vide. Gardez-le en tête sur tout ce qui touche aux chemins ou aux dépendances.

Si vous touchez aux templates🔗

Trois vérifications, dont aucune ne tourne dans un cargo test ordinaire :

cargo test --workspace -- --ignored

cargo run -p crabster-cli -- import-cdl examples/shop.cdl --path /tmp/fmt-shop
(cd /tmp/fmt-shop && cargo fmt --all --check)

(cd /tmp/fmt-shop && RUSTFLAGS= cargo +"$(grep -m1 '^rust-version' Cargo.toml | cut -d'"' -f2)" check --all-targets)

La première est indispensable et facile à oublier. Le test qui génère un projet, le compile et interroge son /health est marqué #[ignore] parce qu'il compile un projet entier — donc cargo test --workspace ne le lance pas. C'est pourtant le seul test qui prouve que les templates produisent du Rust valide : sans lui, vous pouvez casser un template et voir toute la suite au vert.

EN: CI runs the same commands, with warnings denied. Note that cargo test --workspace skips the end-to-end tests, which are #[ignore]d because they compile whole generated projects — yet they are the only tests proving the templates produce valid Rust. Whenever you touch the templates, run all three commands above: the ignored tests, a rustfmt check on a generated project, and a compile of it on the Rust version it declares (which is not the generator's).

Contre PostgreSQL et MySQL, en local🔗

Les trois vérifications ci-dessus compilent ; elles n'exécutent rien contre les deux autres bases. C'est là que se cachent les fautes qui ne se voient pas autrement — un ordre de migrations que SQLite accepte et que les deux autres refusent, un type qui perd ses décimales, une migration supprimée au second crabster apply. Chacune de ces trois-là a été trouvée en exécutant, jamais en compilant.

MySQL vient du docker-compose.yml que le projet généré livre lui-même — donc si les identifiants et le .env.example cessent de s'accorder, ça se voit ici :

crabster import-cdl examples/shop.cdl --path /tmp/my-shop --database mysql
cd /tmp/my-shop && cp .env.example .env
docker compose up -d --wait db

TEST_DATABASE_URL="$(grep '^DATABASE_URL=' .env | cut -d= -f2-)" RUSTFLAGS= cargo test

RUSTFLAGS= cargo run

PostgreSQL, sans Docker, avec un cluster à soi :

PG=$(brew --prefix postgresql@14)/bin
$PG/initdb -D /tmp/pgdata -U postgres --auth=trust -E UTF8 --locale=C
mkdir -p /tmp/pgsock
$PG/pg_ctl -D /tmp/pgdata -o "-p 5439 -k /tmp/pgsock -h 127.0.0.1" -l /tmp/pg.log start
$PG/psql -h 127.0.0.1 -p 5439 -U postgres -c "CREATE DATABASE shop" \
    -c "ALTER USER postgres PASSWORD 'postgres'"

puis la même chose que pour MySQL, avec --database postgres et un .env pointant sur le port choisi. À la fin : $PG/pg_ctl -D /tmp/pgdata stop et docker compose down -v.

Et faites-le deux fois. Le parcours qui compte le plus est celui qui enchaîne deux crabster apply : le premier passe toujours, et c'est le second qui a montré que les migrations qu'il écrit n'étaient pas gelées.

EN: the three checks above compile; they run nothing against the other two databases, and that is where the faults live that nothing else shows — a migration order SQLite accepts and the other two refuse, a type that loses its decimals, a migration deleted by the second crabster apply. MySQL comes from the generated project's own Compose file; PostgreSQL can be a cluster of your own on a spare port. Run the walkthrough twice: the second apply is the one that finds things.

Style de code🔗

Une règle spécifique aux générateurs🔗

Le code Rust des templates (crates/crabster-codegen/templates/) est du code destiné à être copié chez l'utilisateur. Il est donc jugé plus sévèrement que le code du générateur lui-même : il doit être lisible par quelqu'un qui découvre Rust, sans astuce ni abstraction gratuite. Un générateur qui produit du code que son utilisateur ne comprend pas a échoué.

Corollaire structurant pour la V2 : dans les templates, séparez nettement le code purement généré du code destiné à être édité par l'utilisateur. C'est ce qui rendra la fusion incrémentale (Phase 11) praticable. Ne pas le faire maintenant coûtera très cher plus tard.

EN: Template code is held to a higher standard than generator code — it must be readable by a Rust newcomer. And keep purely-generated code clearly separated from user-editable code: V2's incremental merging depends on it.

Commits et Pull Requests🔗

Politique de versionnement🔗

Crabster verse deux artefacts au cycle de vie distinct, et c'est délibéré :

ArtefactVersionnementCe qu'une rupture signifie
Le générateur (crates crabster-*)SemVer classiqueLa CLI, ses options, ou l'API publique des crates changent de façon incompatible
Les templates (crates/crabster-codegen/templates/)Version propre, suivie dans les projets générésLe code généré change de façon qui casse un projet existant lors d'une mise à jour

Pourquoi les séparer : ajouter une sous-commande à la CLI n'affecte en rien un projet déjà généré, tandis que restructurer un template peut casser la mise à jour de milliers de projets sans que la CLI ait changé d'un octet. Les confondre rendrait le numéro de version ininterprétable pour l'utilisateur.

Concrètement, depuis la Phase 3, chaque projet généré porte les deux numéros dans son .crabster/project.toml : generator, et templates. C'est le second qui compte pour crabster record, qui refuse un projet dont la version de templates n'est pas la sienne — rendre à nouveau des templates qui ont bougé ferait passer tout le projet pour modifié à la main. Un correctif de CLI qui ne touche à aucun template laisse donc les projets existants intacts, ce qui est précisément la raison d'être de la séparation.

Franchir cette version plutôt que la refuser, c'est crabster upgrade : il régénère depuis .crabster/, réécrit ce que personne n'a touché, et fusionne le reste avec ce que son propriétaire a écrit. Donc : si vous touchez à un template, montez TEMPLATES_VERSION — c'est ce numéro qui dit d'où vient un projet, et c'est ce qui donne à upgrade quelque chose à franchir.

Avant la 1.0, conformément à SemVer, les versions 0.x peuvent introduire des ruptures sur une version mineure. Elles seront listées dans le CHANGELOG.

EN: The generator and the templates are versioned separately, because adding a CLI subcommand doesn't affect an already-generated project, whereas restructuring a template can break upgrades for thousands of projects without the CLI changing at all. Since Phase 3 both numbers are stamped into every generated project's .crabster/project.toml; the templates one is what crabster record refuses on. Crossing it rather than refusing it is crabster upgrade. So: if you touch a template, bump TEMPLATES_VERSION — it is what says where a project came from, and what gives upgrade something to cross.

Ce qu'un blueprint peut casser🔗

La version de templates sert deux publics, et le second est plus facile à oublier. Un projet généré est rattrapé par crabster upgrade ; un blueprint, lui, vit dans un autre dépôt, appartient à quelqu'un d'autre, et n'apprend qu'une chose a bougé que le jour où il ne génère plus.

Ce contre quoi un blueprint est écrit tient en cinq sortes de noms : les modules qu'il peut requérir, les fichiers qu'ils engendrent, les slots à l'intérieur de ces fichiers, les régions protégées, et les variables de contexte. Aucun n'est déclaré nulle part comme une promesse — ce sont simplement ceux que les templates contiennent.

Ils le sont désormais. crates/crabster-codegen/blueprint-contract.txt les énumère, et un test compare cet instantané aux templates tels qu'ils sont :

Ce que vous faitesCe qu'il faut
Ajouter un slot, un fichier, un module, une variableRien de cassé. Ré-enregistrez l'instantané ; un correctif suffit
Renommer ou supprimer l'un d'euxRupture pour les blueprints. Montez TEMPLATES_VERSION sur une mineure, dites ce qui a bougé dans son historique, et ré-enregistrez
Changer ce qu'un template engendre sans toucher à ces nomsRupture éventuelle pour les projets, pas pour les blueprints. La règle ci-dessus s'applique telle quelle
cargo run -p crabster-cli -- blueprint contract > crates/crabster-codegen/blueprint-contract.txt

Le test ne juge pas le changement — avant la 1.0 une mineure a le droit de tout casser. Il le rend visible au moment où on le fait, plutôt qu'à la génération de quelqu'un d'autre.

Côté auteur de blueprint, crabster blueprint check <dir> répond à la même question dans l'autre sens : est-ce que ce blueprint nomme encore des choses qui existent, sans rien générer pour le savoir.

EN: The templates version serves two audiences, and the second is easier to forget: a generated project is caught up by crabster upgrade, while a blueprint lives in somebody else's repository and learns that something moved the day it stops generating. blueprint-contract.txt records every name a blueprint can be written against and a test compares it against the templates. Adding is a patch; renaming or removing is a minor bump and a CHANGELOG entry. crabster blueprint check asks the same question from the author's side.

Licence des contributions🔗

En soumettant une contribution, vous acceptez qu'elle soit distribuée sous la double licence MIT OR Apache-2.0 du projet, sans conditions supplémentaires. Voir LICENSE.md.

Notez en particulier la clause sur le code généré : il appartient à l'utilisateur du générateur. Toute contribution aux templates est faite en connaissance de ce fait.

EN: Contributions are dual-licensed MIT OR Apache-2.0. Note the generated-code clause in LICENSE.md: generated output belongs to the user.

Signaler un problème de sécurité🔗

Ne pas ouvrir d'issue publique. Passez par le signalement privé de GitHub — Security → Report a vulnerability — décrit dans SECURITY.md, qui dit aussi ce qui relève du code généré et ce qui relève du générateur : les deux n'ont pas la même gravité.

EN: Do not open public issues for security problems. Use GitHub's private vulnerability reporting, described in SECURITY.md. {% endraw %}