É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 voulez | Ce que vous déposez |
|---|---|
| Remplacer un template | Un fichier au même chemin, dans un répertoire du même nom de module |
| Ajouter un fichier à un module existant | Un fichier de nom inédit, dans un répertoire du même nom de module |
| Ajouter un module entier | Un 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*.rsattraperait aussimod.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 :
| Variable | Ce que c'est |
|---|---|
project_name | Le nom du crate généré |
database | postgres, mysql ou sqlite |
database_display | PostgreSQL, MySQL, SQLite |
database_url | La chaîne de connexion d'exemple |
database_name | Le nom de base, - remplacé par _ |
server_port | Le port d'écoute |
sea_orm_driver | La feature SeaORM correspondante |
timestamp_column | La colonne qu'un Timestamp devient sur cette base |
rust_version | Le 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 :
- Les liens symboliques sont ignorés. Un blueprint ne peut pas aspirer des fichiers de la machine qui génère.
- Une destination qui sort du projet est refusée.
- Un chemin de blueprint inexistant, ou un répertoire sans aucun module, fait échouer la commande plutôt que de retomber silencieusement sur les modules du cœur — sans quoi une faute de frappe dans le chemin générerait le projet intégré et annoncerait un succès.
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🔗
| Blueprint | Ce qu'il fait | Templates | Livré comment | Maintenu par |
|---|---|---|---|---|
| React | Un frontend React au-dessus du client TypeScript généré, avec écran de connexion sous auth jwt | 0.23.0 | dans le binaire — ui react suffit | Le projet Crabster |
| Vue | Le même écran en Vue, au-dessus du même client | 0.23.0 | dans le binaire — ui vue | Le projet Crabster |
| Angular | Le même en Angular — composants autonomes et signaux, sans service HttpClient par-dessus le client | 0.23.0 | dans le binaire — ui angular | Le projet Crabster |
| GitLab CI | Un pipeline GitLab à la place du GitHub | 0.23.0 | --blueprint | Le projet Crabster |
| Diesel | Diesel à 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 | --blueprint | Le 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 %}