Aller au contenu

Le routeur dans Forge

Ce document explique le routeur HTTP de Forge : déclarer des routes, les regrouper, les nommer et générer leurs URLs avec le module core.http.router.

1. Rôle

core.http.router associe une méthode HTTP et un chemin à un gestionnaire, c'est-à-dire l'action d'un contrôleur.

Le routeur compile chaque route en une expression régulière, capture les segments dynamiques (/client/show/{id}), et résout une requête entrante vers le bon gestionnaire.
Il permet aussi de regrouper des routes partageant des réglages communs (préfixe, accès public, protection CSRF, mode API) et de générer une URL depuis le nom d'une route.

La convention de route (ADR-029) est : chemin /<contrôleur>/<méthode> (l'index est le chemin nu), nom <contrôleur>-<méthode>.

2. Vue d'ensemble rapide

Élément Valeur
Module core.http.router
Couche HTTP
Rôle associer méthode et chemin à un gestionnaire, résoudre les requêtes
Classes publiques Router, RouteGroup, RouteEntry
Type lié Handler (alias Callable[..., Any])
Constantes liées SAFE_METHODS, UNSAFE_METHODS
Exception liée TypeError (handler non appelable), ValueError (nom de route en double), KeyError (route inconnue ou paramètre manquant dans url_for)
Convention ADR-029 (chemin /contrôleur/méthode, nom contrôleur-méthode)

3. Schémas UML

3.1 Diagramme de classe

Le diagramme montre les trois classes du module et leurs relations : le Router détient des RouteEntry, et un RouteGroup ajoute des routes au Router via un préfixe partagé.

classDiagram
    direction LR

    class Router {
        +add(method, pattern, handler, name, public, csrf, api) Router
        +group(prefix, public, csrf, api) RouteGroup
        +match(method, path) tuple|None
        +resolve(method, path) tuple|None
        +is_public(path, method) bool
        +iter_routes() list~RouteEntry~
        +url_for(name, **params) str
    }

    class RouteGroup {
        +str prefix
        +add(method, pattern, handler, ...) RouteGroup
    }

    class RouteEntry {
        +str|list method
        +str pattern
        +Handler handler
        +str|None name
        +bool public
        +bool csrf
        +bool api
        +matches_method(method) bool
        +match(path) dict|None
        +requires_csrf(method) bool
        +method_label
    }

    Router "1" o-- "0..*" RouteEntry : détient
    Router --> RouteGroup : ouvre via group()
    RouteGroup --> Router : ajoute des routes

À retenir :

  • Router est le point d'entrée : il enregistre les routes et ouvre des groupes ;
  • chaque route déclarée devient un RouteEntry compilé en expression régulière ;
  • un RouteGroup factorise un préfixe et des réglages, puis délègue à Router.add.

3.2 Diagramme de séquence

Le diagramme montre la résolution d'une requête entrante vers un gestionnaire.

sequenceDiagram
    participant Forge as Application Forge
    participant Router as Router
    participant Entry as RouteEntry

    Forge->>Router: resolve("GET", "/client/show/42")
    Router->>Router: match(method, path)
    loop pour chaque route déclarée
        Router->>Entry: matches_method("GET") ?
        Router->>Entry: match("/client/show/42") ?
        Entry-->>Router: {"id": "42"} ou None
    end
    Router-->>Forge: (handler, {"id": "42"})

À retenir :

  • match parcourt les routes dans l'ordre de déclaration et retourne la première qui correspond ;
  • resolve renvoie le couple (handler, params) prêt à appeler ;
  • les segments dynamiques capturés sont retournés sous forme de dictionnaire (vide pour une route statique).

4. API publique

Router

Élément Signature Rôle
add add(method: str | list[str], pattern: str, handler: Handler, *, name: str | None = None, public: bool = False, csrf: bool = True, api: bool = False) -> Router enregistre une route, retourne self pour le chaînage
group group(prefix: str, *, public: bool = False, csrf: bool = True, api: bool = False) -> RouteGroup ouvre un groupe partageant un préfixe et des réglages
match match(method: str, path: str) -> tuple[RouteEntry, dict[str, Any]] | None trouve l'entrée correspondante et ses paramètres
resolve resolve(method: str, path: str) -> tuple[Handler, dict[str, Any]] | None trouve le gestionnaire et ses paramètres
is_public is_public(path: str, method: str | None = None) -> bool indique si le chemin correspond à une route publique
iter_routes iter_routes() -> list[RouteEntry] retourne les routes dans l'ordre de déclaration
url_for url_for(name: str, **params: Any) -> str génère l'URL d'une route nommée (segments URL-encodés)

RouteGroup

Élément Signature Rôle
add add(method: str | list[str], pattern: str, handler: Handler, *, name=None, public=None, csrf=None, api=None) -> RouteGroup ajoute une route héritant des réglages du groupe

Un réglage laissé à None dans RouteGroup.add hérite de la valeur du groupe.

RouteEntry

Élément Signature Rôle
RouteEntry RouteEntry(method, pattern, handler, *, name=None, public=False, csrf=True, api=False) une route compilée (méthode(s), motif, gestionnaire, indicateurs)
matches_method matches_method(method: str) -> bool vrai si la route accepte cette méthode
match match(path: str) -> dict[str, Any] | None paramètres capturés, ou None si le chemin ne correspond pas
requires_csrf requires_csrf(method: str) -> bool vrai si la protection CSRF s'applique pour cette méthode
method_label propriété libellé lisible de la ou des méthodes

Signification des indicateurs d'une route :

Indicateur Valeur par défaut Rôle
public False route accessible sans authentification
csrf True protection CSRF exigée sur les méthodes non sûres
api False route d'API (réponses JSON, pas de redirection login)
name None nom stable de la route (convention contrôleur-méthode)

Motifs de chemin reconnus :

Motif Sens
/clients route statique exacte
/clients/{id} segment dynamique nommé
/clients/{id}/edit segment dynamique en position intermédiaire

5. Contextes d'utilisation

Besoin Élément
Déclarer les routes de l'application mvc/routes/, un fichier par contrôleur, par groupes
Séparer routes publiques et protégées router.group(..., public=True)
Ajouter les routes d'un opt-in register_<module>_routes(router)
Résoudre une requête vers un gestionnaire router.resolve(method, path)
Générer l'URL d'une route nommée router.url_for(name, **params)

6. Exemples d'utilisation

Déclarer des routes nommées et un groupe public :

from core.http.router import Router

router = Router()

# Convention ADR-029 : chemin /<contrôleur>/<méthode>, nom <contrôleur>-<méthode>.
router.add("GET", "/", HomeController.index, name="home-index")
router.add("GET", "/client/show/{id}", ClientController.show, name="client-show")

with router.group("", public=True) as public:
    public.add("GET", "/login/form", LoginController.form, name="login-form")
    public.add("POST", "/login/login", LoginController.login, name="login-login")

Résoudre une requête et générer une URL :

result = router.resolve("GET", "/client/show/42")
# (ClientController.show, {"id": "42"})

url = router.url_for("client-show", id=42)
# "/client/show/42"

Validation à l'enregistrement

Un handler non appelable est rejeté dès add, avec une TypeError qui pointe la ligne fautive de routes.py.

Un nom de route déjà utilisé lève une ValueError, ce qui évite les collisions silencieuses.

URL-encodage des segments

url_for encode chaque paramètre, y compris /, espace, ? ou #.

Un paramètre manquant lève une KeyError listant les segments non résolus.

Voir aussi