Framework web pour ALEX, au-dessus de web.server : routes par
décorateurs, gestionnaires async, groupes, intergiciels async, validation des entrées par schéma, WebSocket,
Server-Sent Events, plusieurs processus, rechargement à chaud, calcul délégué à des fils, base de données attendue
avec await, dépendances déclarées, configuration par profils, client de test en mémoire, gabarits, sessions,
CSRF, formulaires et téléversements, authentification (mots de passe, rôles, JWT), courriels (SMTP, gabarits,
confirmation d'adresse, mot de passe oublié), tâches de fond persistantes et planifiées, ORM léger et migrations sur
SQLite, PostgreSQL ou ALEX DB, salons temps réel avec présence, description OpenAPI 3.1, limitation de débit partagée
entre processus, versionnage d'API et pagination, HTTP/2, HTTPS sans OpenSSL (TLS 1.3 écrit en ALEX), et
compilation en exécutable natif ; avec la commande
alweb (modèle de projet, lancement, tests, routes, migrations, construction, déploiement). Version 0.9.0
(phases 1 à 7 de la feuille de route terminées).
ln -s "$PWD/bin/alweb" ~/.local/bin/alweb # Windows : ajoutez le dossier bin au PATH (bin\alweb.cmd)
alweb new monsite # projet prêt à lancer, dépendances installées
cd monsite
alweb run # http://127.0.0.1:8000/ (debug, rechargement à chaud)
alweb run --https # https://127.0.0.1:8000/ (certificat de développement, h2)
alweb testPour un premier essai complet sur votre machine (ALEX 1.4.0, sh tools/essai_local.sh, site
vitrine avec thème) : docs/ESSAI_LOCAL.md.
Pour découvrir tout le framework : le guide d'alweb ; pour apprendre en pratique : le
tutoriel du blog ; pour la commande : docs/COMMANDE.md ;
pour la production : docs/DEPLOIEMENT.md.
use alweb
use serialization.schema as sch
app := alweb.App("notes")
NOTE := sch.object({"titre": sch.string(1, 120)}, ["titre"])
@app.get("/")
func accueil(req):
return "<h1>Notes</h1>"
@app.get("/notes/{id:int}", {"name": "note"})
async func voir(req):
note := await base.trouver(req.params["id"]) # le serveur sert les autres requêtes pendant l'attente
if note == null:
alweb.abort(404, "note introuvable")
return note # dict -> JSON
@app.post("/notes", {"body": NOTE})
async func creer(req):
return (await base.creer(alweb.data(req)), 201)
await app.run("127.0.0.1", 8000)ALEX 1.4.0 ou plus récent : le compilateur prêt à l'emploi des versions publiées, ou construit depuis les sources :
cd alex-lang && make alex # environ 8 minLe premier make alex part de la graine et demande plus de 6 Go de mémoire : sur une machine plus juste, ajoutez
de l'espace d'échange (fallocate -l 8G /swapfile && mkswap /swapfile && swapon /swapfile).
Pour ALEX DB (facultatif) : le projet AlexDB 0.9.0 (clés automatiques, last_insert_id, résumé par
instruction des requêtes groupées), voir docs/ALEXDB.md.
| Besoin | Écriture |
|---|---|
| Route | @app.get("/x"), app.post("/x", gestionnaire) ; aussi put, patch, delete, any, route([méthodes], ...) |
| Options d'une route | dictionnaire : {"name": "x", "body": SCHEMA, "query": SCHEMA, "middleware": [m]} (clé inconnue refusée) |
| Paramètres | req.params["id"] (types int, float, slug, uuid, path dans le motif) |
| Données validées | alweb.data(req) (corps), alweb.data(req, "query") ; refus : 422 JSON avec details |
| Réponses | Response, str (HTML si « < »), dict / list (JSON), (corps, statut[, en-têtes]), null (204), alweb.json/html/text/redirect/file |
| Erreurs | alweb.abort(404, "message") ; @app.error_handler(404) |
| Groupes | api := app.group("/api", [garde]) ; api.group("/admin") |
| Intergiciels | app.middleware([...]) ; async func m(req, suivant) avec await suivant(req) ; sans attente : alweb.hook(avant, après) (aucune tâche créée) |
| Prêts à l'emploi | alweb.middleware : request_id, timing, logger("text"/"json"), security_headers, cors |
| Cycle de vie | @app.on_startup, @app.on_shutdown (async permis) |
| Statique | app.static("/static", "public") |
| WebSocket | @app.websocket("/chat/{salon}") puis async func chat(req, ws) : await ws.receive(), await ws.send_text(...) ; options protocols, max_message, origins, name |
| SSE, flux | app.get("/evts", alweb.sse(producteur)) : await flux.send(données, événement, id) ; alweb.stream(producteur, type) : await flux.write(morceau) ; flux.closed, flux.last_event_id |
| Lancement | await app.run(hôte, port) ; app.server(hôte, port) rend le web.server.Server ; HTTPS et HTTP/2 : app.run(hôte, 8443, alweb.https("cert.pem", "cle.pem")) |
| Plusieurs cœurs | await app.run_workers(hôte, port, n) (0 : un par cœur ; démarrage et arrêt dans chaque ouvrier) |
| Développement | await app.run_dev() : mode debug, relancé à chaque modification d'un .al du projet |
| Calcul lourd | await alweb.offload(module.fonction, [args]) : sur un autre cœur, sans bloquer le serveur |
| Base de données | db := alweb.database("sqlite:app.db") (ou postgres://…, alexdb://…) puis await db.query(...), insert, transaction... |
| HTTP sortant | await network.async_client.get(url) dans un gestionnaire |
ALEX ne transmet pas d'arguments nommés aux appels de méthodes : les options passent par un dictionnaire.
app.provide("db", ouvrir_base, "app", fermer_base) # une fois au démarrage : fn(app) ; fermé à l'arrêt
app.provide("user", utilisateur) # une fois par requête, si demandé : fn(req)
async func utilisateur(req):
db := await alweb.dep(req, "db") # un fournisseur peut dépendre d'un autre
u := await db.query_one("SELECT * FROM comptes WHERE jeton = ?", [req.header("x-jeton")])
if u == null:
alweb.abort(401, "jeton invalide")
return u
@app.get("/moi", {"deps": ["user"]}) # résolu avant l'appel, après la validation
func moi(req):
return req.deps["user"]
api := app.group("/api", null, ["user"]) # dépendances communes à un groupe
app.override("user", (req) => {"nom": "Test"}) # en testCache par requête, fournisseurs async, teardown même en cas d'erreur, cycles détectés, dépendance inconnue refusée dès la déclaration de la route.
use alweb.sessions as sessions
use alweb.csrf as csrf
use alweb.auth as auth
use alweb.orm as orm
ARTICLES := orm.table("articles", {"id": orm.pk(), "titre": orm.text(120), "auteur_id": orm.ref("comptes")})
app.middleware([sessions.middleware(SECRET), csrf.middleware(["/api/"])]) # session signée, puis CSRF
app.use_templates("templates") # {% extends %}, {% block %}...
app.provide("user", auth.current_user(charger)) # charger(req, id) -> compte
@app.post("/ecrire", {"middleware": [auth.login_required()], "deps": ["db", "user"]})
async func ecrire(req):
f := alweb.validate_form(req, ARTICLE) # erreurs par champ
if not f.valid:
return alweb.render(req, "article_form.html", {"form": f}, 422)
id := await ARTICLES.insert(req.deps["db"], {"titre": f.data["titre"], "auteur_id": req.deps["user"]["id"]})
alweb.session(req).flash("Article publié.", "succes")
return alweb.redirect(app.url_for("article", {"id": id}), 303)| Module | Contenu |
|---|---|
alweb.templates |
syntaxe à la Jinja : `{{ x |
alweb.sessions |
middleware(secret, store) : cookie signé (défaut), MemoryStore, DatabaseStore(db) ; get/set/pop, flash, regenerate, invalidate |
alweb.csrf |
jeton par session, champ csrf_token ou en-tête X-CSRF-Token ; exemptions par préfixe et pour les appels Authorization sans cookie ; {{ csrf_field() }} |
alweb.forms |
alweb.form(req) (urlencoded, multipart), validate_form(req, schéma) (messages rédigés pour le visiteur, dans sa langue ; f.codes, f.details), check_upload(fichier, octets, types) (type réel lu dans le contenu), unique_name |
alweb.auth |
hash_password/verify_password (PBKDF2 hors de la boucle), login (nouvelle session et nouveau jeton CSRF), logout, login_required, roles_required, current_user, next_url (sûr), jwt_encode/jwt_decode (HS256), bearer(secret) ; liens par courriel : reset_token / check_reset_token (usage unique), confirm_token / read_confirm_token, signed_token / read_token |
alweb.jobs |
Queue(app, DatabaseStore(() => app.value("db"))) : tâches en base (SQLite, PostgreSQL, ALEX DB) partagées entre processus, @q.task(nom), enqueue(nom, données, {delay, at, attempts, key}), nouveaux essais espacés puis abandon, délai d'exécution, reprise des tâches interrompues ; @q.every(nom, "30 3 * * *") ou every(nom, 300) : une exécution par créneau tous processus confondus ; send_later des courriels y passe ; alweb jobs (état, --retry) et page « Tâches » de l'administration ; tests : at.run_jobs(app) (guide) |
alweb.i18n |
i18n.I18n(app, "locales", {"default": "fr"}) : catalogues locales/<langue>.json (clés = texte de la langue par défaut, repli fr-CA → fr → défaut), langue de chaque requête (préfixe /en/…, ?lang=, compte, session, cookie, Accept-Language), pluriels CLDR, dates, nombres, montants (XOF, EUR…), temps relatif ; gabarits : _(), _n(), | date, | currency, locales (sélecteur) ; courriels dans la langue du destinataire ; erreurs de formulaire, pages d'erreur (fr, en, es, de, it, pt, nl) et administration (fr, en) traduites par alweb ; alweb i18n (traductions manquantes) (guide) |
alweb.storage |
storage.setup(app) (clé storage.url) : disque (file:media), mémoire (tests), compatible S3 (s3://CLÉ:SECRET@hôte/seau?region=… : S3, MinIO, Scaleway, B2, R2 ; signature AWS V4 en ALEX) ; put, get, head, list, delete, URL signées qui expirent (st.url(clé, secondes, nom) : présignée S3, ou route de l'application), save_upload (type réel vérifié) (guide) |
alweb.images |
vignettes et tailles d'une image (fit, cover ; JPEG, PNG, GIF, WebP en entrée ; orientation Exif) par un programme natif écrit en ALEX dans un processus à part ; images.store(st, fichier, {"sizes": {...}}) enregistre l'original et ses tailles |
alweb.themes |
thèmes installables (ALWEB Templates) : alweb-theme.json (manifeste, versions et contraintes), paramètres typés (config/theme.json, choix du projet dans config/theme.<slug>.json, variables CSS), validation de sécurité (pas de code ni d'exécutable, liens symboliques, SVG piégés, tailles, gabarits et références), archives .alwt reproductibles, installation hors ligne dans themes/ avec alweb.lock, activation, retour arrière ; themes.setup(app) : pages du thème, templates/ de l'application prioritaire, asset() versionné ; alweb theme (create, validate, package, preview, install…) ; page « Apparence » de l'administration : réglages avec aperçu en direct, historique, logo envoyé ; client de marketplace (alweb.themes.client, protocole ouvert que tout serveur peut suivre) : publication signée ECDSA P-256, recherche, installation vérifiée avec clé de l'éditeur épinglée, mises à jour, retrait de version, avis (guide) |
alweb.mail |
alweb.mailer(req) / app.mail() réglé par la clé mail : SMTP (smtp://, smtps://, STARTTLS, jamais d'authentification en clair, envoi sur un fil), console (défaut en debug), memory (tests : at.outbox(app), at.links(texte)), file:dossier ; send, send_template (emails/<nom>.txt / .html), send_later, pièces jointes, Cc / Cci ; absolute_url(req, chemin) par site_url (guide) |
alweb.orm |
table, colonnes pk/integer/real/text/boolean/timestamp/json/ref, where/order_by/limit/offset, with_one/with_many (sans N+1), paginate, update, delete, validation, migration, index, add_column (colonne ajoutée à une table existante) et migrate (une transaction par migration) ; dialectes SQLite, PostgreSQL et ALEX DB |
Dans les gabarits rendus par alweb.render : request, config, session, flashes(), csrf_token(),
csrf_field(), url_for(nom, valeurs), app_name, et ce que rendent les @app.context_processor.
use alweb.realtime as rt
use alweb.ratelimit as rl
use alweb.openapi as oa
use alweb.pagination as pg
hub := rt.Hub() # un processus ; plusieurs : rt.Hub(rt.DatabaseBroker(db))
hub.attach(app)
@app.websocket("/ws/{salon}")
async func salon(req, ws):
await hub.room(req.params["salon"]).serve(ws, {"id": pseudo}) # présence, diffusion, départ
app.get("/salons/{salon}/evenements", hub.sse((req) => req.params["salon"])) # SSE, reprise Last-Event-ID
app.middleware(rl.limit(600, 60)) # par IP ; rl.DatabaseStore(db) : commun aux processus
v1 := app.group("/api/v1", null, null, {"tags": ["v1"], "deprecated": 1767225600, "sunset": 1782864000})
v1.get("/messages", liste, {"summary": "Messages", "response": PAGE, "cors": ["https://lecteur.example"]})
oa.setup(app, title="Messagerie", version="1.0.0") # GET /openapi.json et GET /docs| Module | Contenu |
|---|---|
alweb.realtime |
Hub(courtier), hub.room(nom) : join, leave, broadcast, members, count, serve(ws, membre, on_message) ; hub.publish, hub.history, hub.sse(salon) ; MemoryBroker(historique), DatabaseBroker(db) (SQLite sur fichier, PostgreSQL ou ALEX DB : événements, présence et historique communs à tous les processus, livrés dans le même ordre partout) |
alweb.ratelimit |
limit(n, période, clé, stockage, scope, fail_open, exempt) : fenêtre glissante, en-têtes RateLimit-*, 429 avec Retry-After ; clés by_ip(), by_header(nom) ; MemoryStore, DatabaseStore(db) |
alweb.openapi |
spec(app, …) (dict OpenAPI 3.1, filtrable par préfixe de version), setup(app, …) (/openapi.json et page /docs autonome avec « Essayer »), component(nom, schéma) ($ref), json_schema(schéma) |
alweb.pagination |
params(req), page(req, résultat) (Link, X-Total-Count), cursor(req), cursor_page(req, items, suivant), encode_cursor/decode_cursor |
Options de route ajoutées : summary, description, tags, response, status, responses, security,
operation_id, hidden (documentation) ; deprecated et sunset (en-têtes Deprecation et Sunset) ;
cors (origines permises et pré-vérification OPTIONS pour ce seul motif). app.group(préfixe, intergiciels, dépendances, options) applique tags, deprecated, sunset, security et cors à toutes ses routes.
app.value(nom) rend une dépendance de portée app déjà créée.
HTTP/2. Sans réglage, un client qui commence par la préface HTTP/2 (curl --http2-prior-knowledge, mandataire
inverse en h2c) est servi en HTTP/2 sur le port HTTP/1.1. Sur TLS, alweb.https(cert, clé) propose h2 par ALPN
(navigateurs). Même application, mêmes intergiciels : req.version vaut "HTTP/2". Flux multiplexés,
gestionnaires async en parallèle, fichiers, SSE ; pas de push serveur ni de WebSocket sur HTTP/2 (les navigateurs
ouvrent alors une connexion HTTP/1.1). Conformité : h2spec 146/146 sur TLS.
Fichiers statiques. Un gros fichier ne bloque plus la boucle : il part par morceaux quand le client lit. Posez
site.css.br ou site.css.gz à côté de site.css : ils sont servis aux clients qui les acceptent.
Gabarits. Compilés en fermetures au chargement : environ 40 % plus rapides (50 µs par ligne de tableau en VM, 30 µs en natif, contre 85 µs).
Exécutable natif.
alexc build examples/chat.al --native -o messagerie # un exécutable autonome (environ 15 Mo)
ALWEB_ENV=prod ALWEB_DATABASE__URL=postgres://… ./messagerieLe natif ne charge pas de bibliothèque C : pas de SQLite (FFI) ni d'OpenSSL. ALEX DB et PostgreSQL fonctionnent
(pilotes écrits en ALEX), et HTTPS aussi : TLS 1.3 écrit en ALEX (voir « HTTPS natif » plus bas).
Les mots de passe (PBKDF2) sont calculés par le runtime natif. Débit mesuré : web.server natif environ 2 fois la référence de la phase 0, alweb avec
trois intergiciels 2 300 à 2 400 req/s contre 1 680 en VM et 878 en phase 1 (bench/RESULTS.md).
| Élément | Contenu |
|---|---|
| Paquet | alweb/ est le paquet installable (son propre alex.toml) : alweb = { path = "…/alweb/alweb" } ou alweb = "^0.9.0" après alexc pkg publish ; alexc pkg install le recopie dans alex_packages/alweb/ |
| Commande | bin/alweb, bin/alweb.cmd : new, run, test, routes, migrate, i18n, jobs, theme, check, build, deploy, secret (référence) |
| Modèle de projet | alweb/modele/ : main.al par profil, application.al (creer(cfg, dossier)), modeles.al, config/ dev / test / prod avec clés tirées au hasard, gabarits, tests |
| Tutoriel | docs/TUTORIEL_BLOG.md, résultat complet et testé dans docs/tutoriel/blog/ |
| Déploiement | docs/DEPLOIEMENT.md ; alweb deploy systemd caddy nginx docker génère service systemd durci, Caddyfile, configuration nginx, Dockerfile et compose.yaml |
| Mandataires | server.trusted_proxies dans la configuration : adresse réelle du client derrière Caddy / nginx (journaux, limites de débit) |
alweb routes # MÉTHODES MOTIF NOM
alweb migrate --status # [x] 1 messages
alweb build # dist/<nom> : exécutable autonome (VM embarquée, SQLite)
alweb deploy systemd caddy --domaine monsite.example.orgALEX DB, le SGBD écrit en ALEX, est la troisième base d'alweb, à côté de SQLite et PostgreSQL :
use alweb.alexdb as axdb
url := "alexdb://admin:mdp@127.0.0.1:5442/blog"
await axdb.ensure_database(url) # CREATE DATABASE IF NOT EXISTS (super-utilisateur)
db := alweb.database(url)
await orm.migrate(db, MIGRATIONS) # dialecte « alexdb » : INT PRIMARY KEY attribuée par le serveur
id := await ARTICLES.insert(db, {"titre": "Été"}) # identifiant rendu, jamais réattribué
await db.transaction([...]) # une seule requête BEGIN … COMMIT : atomique, sans verrou croisé| Élément | Contenu |
|---|---|
| Pilote | alexdb://utilisateur:mdp@hôte:5442/base?connect_timeout=10&lock_timeout=5 : paramètres ? / :nom, identifiants, transactions d'un bloc, LockError réessayé hors transaction, erreurs IntegrityError / QueryError / ConnectionError ; écrit en ALEX, donc disponible dans l'exécutable natif (sans SQLite) |
| ORM | orm.dialect(db) vaut "alexdb" : types INT / FLOAT / VARCHAR(n) / TEXT, booléens en 0 / 1 ; orm.index(nom, table, colonnes) (sur ALEX DB, la première colonne) ; migrations dans une transaction (DDL transactionnel) |
| Magasins | sessions.DatabaseStore, ratelimit.DatabaseStore (sans clé composite ni ON CONFLICT : transaction d'un bloc), realtime.DatabaseBroker : partagés entre ouvriers par le serveur |
alweb.alexdb |
ensure_database, with_database, parse, redact, check, Server(home) : serveur jetable pour les tests et les démonstrations |
| Commande | alweb new monsite --base alexdb (ou --base alexdb://…) : config/base.json pointe sur ALEX DB, base créée au démarrage |
| Tests | ALWEB_TEST_ALEXDB=alexdb://… ou ALWEB_ALEXDB_HOME=…/AlexDB : tests/test_alexdb.al ; tools/test_alexdb.sh : toute la suite sur un serveur jetable (ORM, limites, salons, blog, messagerie, ouvriers) ; at.alexdb_database(nom) dans les tests d'une application |
Sans OpenSSL (exécutable natif), web.server termine TLS lui-même avec network.tls13, une implémentation de
TLS 1.3 écrite en ALEX ; le calcul lourd (X25519, P-256 / P-384, RSA, AES-GCM, SHA-256) passe par le runtime typé
(crypto.accel), 30 à 70 fois plus vite que le même calcul en Alex dynamique. HTTPS se configure sans code :
{"server": {"tls": {"cert": "/etc/letsencrypt/live/site/fullchain.pem", "key": "/etc/letsencrypt/live/site/privkey.pem"}}}app.run / app.run_workers servent alors HTTPS, HTTP/2 par ALPN ("http2": false pour l'interdire) ; un
certificat renouvelé est relu à la connexion suivante. alweb.https(cert, clé) reste disponible dans le code.
Ou bien, sans certbot ni mandataire, alweb obtient et renouvelle lui-même un certificat Let's Encrypt (ACME,
défi http-01 sur le port 80, qui redirige aussi vers HTTPS) — voir DEPLOIEMENT :
{"server": {"host": "0.0.0.0", "port": 443,
"tls": {"acme": {"domains": ["site.example.org"], "email": "admin@example.org", "agree_tos": true}}}}| Élément | Contenu |
|---|---|
| Protocole | TLS 1.3 (RFC 8446) : AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305 ; x25519, P-256, P-384 (HelloRetryRequest) ; ALPN, SNI, KeyUpdate ; TLS 1.2 refusé (protocol_version) |
| Certificats | chaîne PEM ; clés PKCS#8, SEC1, PKCS#1 ; EC P-256 / P-384 ou RSA ≥ 2048 bits ; clé contrôlée contre le certificat au démarrage |
| Reprise | avec le TLS ALEX comme avec OpenSSL ; tickets sans état (clé tirée de la clé privée, communs aux ouvriers ; ALEX_TLS_TICKET_KEY) : un client qui revient évite certificat et signature (RSA-2048 : 39 → 263 connexions/s) ; 0-RTT refusé |
| Client | network.tls.connect sans OpenSSL (session= pour reprendre) : chaîne vérifiée jusqu'à ca_file ou au magasin du système (noms, jokers, dates, RSA-PSS), pas de révocation |
| Choix | OpenSSL s'il est là, sinon ALEX ; ALEX_TLS=alex / openssl pour forcer ; tls.backend() |
| Boucle | la poignée de main n'arrête pas le serveur, avec OpenSSL comme avec le TLS ALEX : la boucle lit et écrit sans attendre (un client lent ou muet ne gêne personne) ; avec le TLS ALEX, le calcul (≈ 4 ms en P-256, 40 ms en RSA-2048) part sur un fil de thread.offload ; server.tls.offload = false le garde dans la boucle |
| Certificat | fichiers PEM (cert, key) ou automatique (acme : Let's Encrypt, renouvelé en tâche de fond, servi sans redémarrage) |
| Coût | 1 856 req/s en keep-alive sur un cœur ; poignées de main P-256 : 235 par seconde sur 2 cœurs (bench/RESULTS.md) |
alweb.admin : tableau de bord, listes (recherche, filtres, tri, pages), fiches de création et de modification, suppression confirmée et export CSV, produits depuis les tables de l'ORM.
use alweb.admin as adm
admin := adm.Admin(app, {"roles": ["admin"], "login_url": "/connexion", "on_change": audit})
admin.register(UTILISATEURS, {"exclude": ["mdp"], "display": "nom"})
admin.register(ARTICLES, {"search": ["titre", "texte"], "filters": ["auteur_id"], "display": "titre"})Rôles exigés (aussi par table), CSRF imposé sur chaque POST, no-store, aucun style en ligne (compatible avec la
CSP de security_headers), colonnes exclues jamais montrées, cellules CSV neutralisées contre les formules.
Champs selon le type de colonne, clés étrangères en listes déroulantes, erreurs de l'ORM et d'unicité au champ.
Recherche de l'ORM : ARTICLES.search(["titre", "texte"], "été"). Le blog d'exemple a son /admin.
Pages ajoutées : « Tâches » (alweb.jobs), « Apparence » (thème actif, aperçu en direct) et toute
extension (option extensions, exemple).
cfg := alweb.load_config("config") # base.json < <profil>.json < local.json < variables ALWEB_*
app.configure(cfg) # app.config ; « debug » pilote le mode debug, server.trusted_proxies
# les mandataires dont X-Forwarded-For est cru, server.tls HTTPS
url := cfg.require("database.url") # ConfigError si absentProfil : ALWEB_ENV (dev par défaut, test, prod…). Variables : ALWEB_DATABASE__URL=… remplace
database.url (__ sépare les niveaux), converties selon le type existant (bool, entier, nombre, JSON).
Validation par schéma : alweb.load_config("config", null, SCHEMA). cfg.safe_dict() masque les secrets.
use alweb.testing as at
from testing use assert_eq
async func test_voir():
c := at.Client(app)
r := await c.get("/notes/1")
assert_eq(r.status, 200)
r = await c.post("/notes", {"json": {"titre": "Été"}})
assert_eq(r.status, 201)alexc test tests docs/tutoriel/blog/tests : 213 tests (tests/ et le blog du tutoriel), dont six de bout en bout sur un vrai port, deux sur
plusieurs processus, et un parcours complet de la commande alweb sur un projet généré (build/test_cli/).
ALEX_TLS=alex alexc test … : la même suite avec le TLS écrit en ALEX au lieu d'OpenSSL.
alexc run tools/test_native.al : la même suite compilée en natif (avec ALWEB_TEST_ALEXDB ou ALWEB_TEST_POSTGRES,
puisque le natif n'a pas SQLite) ; HTTPS et h2 sur TLS y sont testés (serveur et client TLS 1.3 en ALEX).
Les fichiers servis (alweb.file, app.static) arrivent dans r.body comme les autres réponses.
ALWEB_TEST_POSTGRES=postgres://… : les tests de l'ORM, des salons, des limites, du blog et de la messagerie
tournent aussi sur PostgreSQL ; ALWEB_TEST_ALEXDB=alexdb://… (ou ALWEB_ALEXDB_HOME=…/AlexDB tools/test_alexdb.sh,
qui lance un serveur jetable) : sur ALEX DB.
alexc run examples/notes.al : API CRUD « notes » et une page HTML (examples/notes_app.al), configurées par
examples/config. Profil dev (défaut) : debug et rechargement à chaud ; ALWEB_ENV=prod : un processus par
cœur, journal JSON ; stockage SQLite (notes.db) fourni comme dépendance.
alexc run examples/blog.al : blog multi-utilisateur (examples/blog_app.al, gabarits et configuration dans
examples/blog/) : inscription et connexion, articles avec illustration, commentaires, droits (le premier
compte est administrateur, les autres ne modifient que leurs articles), pagination, API JSON avec jeton Bearer
(POST /api/jeton, POST /api/articles). SQLite par défaut ; PostgreSQL :
ALWEB_DATABASE__URL=postgres://utilisateur:mdp@hôte/blog alexc run examples/blog.al.
alexc run examples/chat.al : messagerie temps réel (examples/chat_app.al, page et configuration dans
examples/chat/) : comptes et jetons JWT, salons WebSocket avec présence et indicateur de frappe, historique
paginé par curseur, flux SSE en lecture seule, API /api/v1 documentée sur /docs, limites de débit. Avec
ALWEB_ENV=prod, un processus par cœur : salons et limites passent par la base (SQLite, PostgreSQL ou ALEX DB).
Voir bench/RESULTS.md ; générateur de charge : go run bench/load.go -url ... -c 8 -d 8s.
Copyright 2026 Ennuum Group. alweb est distribué sous la licence Apache, version 2.0 (voir aussi
NOTICE) : vous pouvez l'utiliser, le modifier et le redistribuer, y compris dans des applications
propriétaires, en gardant les mentions de licence. Le paquet installable (alweb/) contient les mêmes
fichiers. Les thèmes créés avec alweb theme create reçoivent la licence MIT au nom de leur auteur (fichier
LICENSE du modèle), qu'il peut changer.