crabster

Écrire un blueprint

{% raw %} Un blueprint est un répertoire de modules de génération que Crabster empile au-dessus des siens. Il change ce qu'un projet généré contient sans qu'une ligne du cœur de Crabster ne bouge — c'est la condition posée par le principe 7 de la vision, et la seule façon pour Crabster de servir des gens dont les besoins n'ont pas été anticipés.

Cette page suffit à en écrire un. Si quelque chose vous manque ici, c'est un défaut de cette page.

En un coup d'œil🔗

crabster import-cdl model.cdl --blueprint ./mon-blueprint
mon-blueprint/
├── ci-gitlab/                    ← un module nouveau
│   ├── module.toml
│   ├── .gitlab-ci.yml.tera
│   └── _into/README.md/
│       └── sections.tera         ← écrit dans un fichier de `core`
└── core/                         ← le module `core`, pas une copie
    ├── module.toml
    └── .github/workflows/
        └── ci.yml.tera           ← remplace ce template-là, et lui seul

C'est le blueprint livré avec Crabster, dans examples/blueprints/gitlab/. Il est généré et vérifié en CI à chaque exécution : ce que cette page décrit est ce qui tourne.

Ce qu'un blueprint peut faire🔗

Quatre choses, et rien d'autre. La résolution se fait template par template, jamais module par module.

Ce que vous voulezCe que vous déposez
Remplacer un templateUn fichier au même chemin, dans un répertoire du même nom de module
Ajouter un fichier à un module existantUn fichier de nom inédit, dans un répertoire du même nom de module
Ajouter un module entierUn répertoire avec son module.toml
Écrire dans un fichier qu'un autre module possède_into/<destination>/<slot>.tera

Un template qui ne rend que du blanc n'écrit aucun fichier. C'est ainsi qu'on retire quelque chose : le blueprint d'exemple remplace le workflow GitHub par un template vide, et le projet généré n'en a pas.

La destination se déduit du chemin, en retirant .tera. Elle n'est déclarée nulle part, donc elle ne peut pas diverger.

Le manifeste🔗

Un répertoire n'est un module que s'il contient un module.toml.

name = "ci-gitlab"
description = "Un pipeline GitLab à la place du GitHub"

requires = ["core"]

templates = "0.23.0"

variables = ["project_name", "database", "rust_version"]

write_once = ["src/migration/m*_*.rs"]

[activation]
setting = "ci"
value   = "gitlab"

[[reserves]]
name   = "Pipeline"
reason = "le module `ci-gitlab` déclare ce type"

Une clé inconnue fait échouer le chargement. C'est délibéré : une faute de frappe dans un nom de champ serait sinon un réglage silencieusement ignoré.

write_once — les fichiers qu'une base a déjà exécutés🔗

Une migration est appliquée à une base, et SeaORM décide « appliquée » ou « en attente » au nom seul du module de migration. Réécrire son contenu sans changer son nom change donc ce que le projet attend sans changer la base, et n'en dit rien : la panne arrive à la première requête, loin de la cause.

Un fichier nommé ici n'est jamais réécrit, jamais supprimé, et --force ne l'ouvre pas. Quand de nouveaux templates produiraient autre chose, crabster upgrade laisse le fichier tranquille et écrit ce qu'il aurait produit dans .crabster/incoming/, pour que vous en fassiez une nouvelle migration.

Les modules intégrés déclarent les leurs — record gèle src/migration/m*_*.rs, auth-jwt gèle src/auth/migration.rs. Déclarez les vôtres si votre module génère quoi que ce soit qu'une base exécute. Les listes sont fusionnées et non remplacées : surcharger un template d'un module ne peut pas dégeler les migrations de ce module.

Notez la seconde étoile. m*.rs attraperait aussi mod.rs, qui est la liste des migrations et doit continuer de grandir.

templates — pourquoi c'est obligatoire🔗

Un blueprint écrit contre des slots, des variables et des noms de fichiers que les templates intégrés définissent, et ceux-là bougent : avant la 1.0, une version mineure peut changer n'importe lequel. Un blueprint écrit il y a une version échouerait alors au fond d'un template, sur un slot qui n'existe plus, et le message ne dirait rien de la cause réelle.

Crabster refuse donc un module de blueprint qui n'en déclare pas, et un qui en déclare une autre — comparée sur majeur et mineur ; le niveau de correctif ne change jamais un template. Les modules intégrés, eux, ne le déclarent pas : ils sont livrés dans le même binaire que le numéro auquel on les comparerait.

La version courante est celle que crabster --version affiche pour les templates, et celle qu'écrit .crabster/project.toml de tout projet généré.

Les variables disponibles🔗

Celles que core déclare sont fournies pour tout projet :

VariableCe que c'est
project_nameLe nom du crate généré
databasepostgres, mysql ou sqlite
database_displayPostgreSQL, MySQL, SQLite
database_urlLa chaîne de connexion d'exemple
database_nameLe nom de base, - remplacé par _
server_portLe port d'écoute
sea_orm_driverLa feature SeaORM correspondante
timestamp_columnLa colonne qu'un Timestamp devient sur cette base
rust_versionLe plus haut MSRV que les modules générés demandent

model s'ajoute quand le projet a un modèle de domaine : c'est la vue décrite par view.rs, avec records, enums, migrations et le reste.

Ne branchez pas un template sur {{ database }} pour produire du SQL. La traduction des types vit dans la vue, une fois, et un template qui la refait diverge le jour où la vue change. Brancher dessus pour autre chose — un service dans un fichier Compose, une image dans un pipeline — est en revanche exactement ce qu'il faut faire.

Les slots🔗

Un fichier a un seul propriétaire. Pour y ajouter quelque chose, le propriétaire ouvre un point d'insertion nommé :

{{ slot(name="dependencies") }}

et vous y contribuez depuis un répertoire symétrique de _each_record/ :

mon-blueprint/mon-module/_into/Cargo.toml/dependencies.tera
mon-blueprint/mon-module/_into/README.md/sections.tera

Le répertoire est la destination, le nom de fichier est le slot. Une contribution vers un slot que personne n'ouvre, ou vers un fichier qu'aucun module actif ne génère, fait échouer la génération en nommant les slots connus — c'est le pendant de deny_unknown_fields.

Les slots que les templates intégrés ouvrent sont listés dans templates/README.md.

Les zones protégées🔗

{{ region(name="…") }} ouvre une zone qui appartient au propriétaire du projet généré : ce qui s'y trouve survit à la régénération octet pour octet, et n'est même pas passé à rustfmt. Placez-en une entre des items ou des instructions, jamais à l'intérieur d'une expression — c'est là que rustfmt est le moins fiable, et le moteur refuse la génération si le formatage a déplacé un marqueur.

Ce qu'un blueprint ne peut pas faire🔗

Un blueprint est du code tiers. Trois garde-fous s'appliquent :

Et une limite qui n'est pas un garde-fou : un blueprint ne peut pas ajouter de sous-commande à la CLI ni changer le langage CDL. Il change ce qui est généré, pas ce qui génère.

Distribuer un blueprint🔗

Un blueprint est un répertoire. Distribuez-le comme un dépôt git, ou comme un crate dont vous pointez le répertoire de templates :

git clone https://example.com/mon-blueprint
crabster import-cdl model.cdl --blueprint ./mon-blueprint

--blueprint ne prend qu'un chemin, pas un nom de crate. Le résoudre voudrait dire télécharger et exécuter des templates tiers depuis crates.io au moment de la génération, et c'est une question de chaîne d'approvisionnement qui mérite mieux qu'un effet de bord de cette phase. Cloner ou cargo add puis pointer le chemin met la même décision entre vos mains, en la rendant visible.

Se faire trouver🔗

Publiez le crate avec le mot-clé crabster-blueprint sur crates.io. C'est la convention de découverte, et elle n'a besoin de rien d'autre qu'elle-même. Ouvrez ensuite une pull request ajoutant une ligne à la table ci-dessous.

crabster blueprint new écrit ce manifeste pour vous, parce qu'une convention qu'un document se contente de demander est une convention suivie une fois sur deux :

name = "crabster-blueprint-<le vôtre>"
keywords = ["crabster-blueprint"]
include = ["<module>/**", "README.md", "lib.rs"]
[workspace]

Quatre lignes qui méritent d'être comprises. Le mot-clé est la seule qui doive être exactement celle-là. include demande une ligne par module que vous ajoutez, sans quoi le paquet part sans ses gabarits et personne ne s'en aperçoit avant d'essayer de s'en servir. [workspace] est vide à dessein : sans elle, un blueprint écrit à l'intérieur d'un autre projet est adopté par le workspace de celui-ci, et cargo refuse alors de tourner dans l'un comme dans l'autre. Et lib.rs est un talon, car un paquet doit avoir une cible et rien ne se lie à celle-ci.

Cette table vide suffit quand l'hôte liste ses membres par leur nom. Elle ne suffit pas quand il les désigne par un motif — members = ["crates/*"] et semblables — car le motif capture alors un répertoire devenu racine de workspace à son tour, et toute commande cargo dans l'hôte échoue sur multiple workspace roots found in the same workspace. crabster blueprint new avertit quand il écrit à l'intérieur d'un workspace, et donne le remède :

[workspace]
exclude = ["crates/mon-blueprint"]

Ou écrivez le blueprint ailleurs qu'à l'intérieur d'un autre projet, ce qui est sa place habituelle.

Que le tour complet fonctionne — engendrer, empaqueter, déballer ailleurs, générer — est vérifié par la barrière de ce dépôt plutôt que supposé.

Les blueprints connus🔗

BlueprintCe qu'il faitTemplatesLivré commentMaintenu par
ReactUn frontend React au-dessus du client TypeScript généré, avec écran de connexion sous auth jwt0.23.0dans le binaire — ui react suffitLe projet Crabster
VueLe même écran en Vue, au-dessus du même client0.23.0dans le binaire — ui vueLe projet Crabster
AngularLe même en Angular — composants autonomes et signaux, sans service HttpClient par-dessus le client0.23.0dans le binaire — ui angularLe projet Crabster
GitLab CIUn pipeline GitLab à la place du GitHub0.23.0--blueprintLe projet Crabster
DieselDiesel à la place de SeaORM, sur SQLite ou PostgreSQL — la couche de persistance remplacée, tout ce qui est au-dessus hérité0.23.0--blueprintLe projet Crabster

Pourquoi deux colonnes « livré comment ». Les trois premiers ne font qu'ajouter un module, revendiqué par un réglage que rien d'autre ne réclame : ils sont donc compilés dans le binaire et n'apparaissent que si un modèle écrit ui …. Les deux derniers remplacent un module du cœur par son nom — record pour Diesel, core pour GitLab — ce qui change ce dont tout projet est fait : cela se demande, avec --blueprint, et ne s'installe pas d'office. La règle est tenue par un test, pas par ce paragraphe.

(Le vôtre ici.)

La colonne Templates est la première à lire. Un blueprint déclare la version de templates contre laquelle il est écrit, et Crabster refuse celui qui en nomme une qu'il ne livre pas. Une ligne annonçant autre chose que votre version est un blueprint qui ne chargera pas — c'est tout l'intérêt de la colonne, et la raison pour laquelle ce tableau est comparé aux blueprints eux-mêmes par un test plutôt que tenu à la main.

« Officiel » veut dire quelque chose ici. Un blueprint de ce tableau sous « Le projet Crabster » est généré et construit par la barrière de ce dépôt, à chaque passage, et tenu à ce à quoi le cœur est tenu. Celui qui n'est que référencé est un blueprint communautaire, et la colonne dit lequel est lequel — voir l'ADR-0003. {% endraw %}