Compojure expliqué (dans une certaine mesure)
NB. Je travaille avec Compojure 0.4.1 ( voici le commit de version 0.4.1 sur GitHub).
Pourquoi?
Tout en haut de compojure/core.clj
, il y a ce résumé utile de l'objectif de Compojure:
Une syntaxe concise pour générer des gestionnaires Ring.
Sur un plan superficiel, c'est tout ce qu'il y a à la question «pourquoi». Pour aller un peu plus loin, voyons comment fonctionne une application de style Ring:
Une demande arrive et est transformée en une carte Clojure conformément à la spécification Ring.
Cette carte est canalisée dans une soi-disant «fonction de gestionnaire», qui devrait produire une réponse (qui est également une carte Clojure).
Le mappage de réponse est transformé en une réponse HTTP réelle et renvoyé au client.
L'étape 2 ci-dessus est la plus intéressante, car il est de la responsabilité du gestionnaire d'examiner l'URI utilisé dans la demande, d'examiner tous les cookies, etc. et finalement d'arriver à une réponse appropriée. Il est évidemment nécessaire que tout ce travail soit intégré dans une collection de pièces bien définies; il s'agit normalement d'une fonction de gestionnaire "de base" et d'une collection de fonctions middleware qui l'encapsulent. Le but de Compojure est de simplifier la génération de la fonction de gestionnaire de base.
Comment?
Compojure est construit autour de la notion de «routes». Celles-ci sont en fait implémentées à un niveau plus profond par la bibliothèque Clout (un spin-off du projet Compojure - beaucoup de choses ont été déplacées vers des bibliothèques séparées à la transition 0.3.x -> 0.4.x). Une route est définie par (1) une méthode HTTP (GET, PUT, HEAD ...), (2) un modèle URI (spécifié avec une syntaxe qui sera apparemment familière aux Webby Rubyists), (3) une forme de déstructuration utilisée dans lier des parties de la mappe de requête aux noms disponibles dans le corps, (4) un corps d'expressions qui doit produire une réponse Ring valide (dans les cas non triviaux, il s'agit généralement d'un appel à une fonction distincte).
Cela pourrait être un bon point pour jeter un œil à un exemple simple:
(def example-route (GET "/" [] "<html>...</html>"))
Testons cela au REPL (la carte de requête ci-dessous est la carte de requête Ring valide minimale):
user> (example-route {:server-port 80
:server-name "127.0.0.1"
:remote-addr "127.0.0.1"
:uri "/"
:scheme :http
:headers {}
:request-method :get})
{:status 200,
:headers {"Content-Type" "text/html"},
:body "<html>...</html>"}
Si :request-method
c'était le cas :head
, la réponse serait nil
. Nous reviendrons sur la question de savoir ce que nil
signifie ici dans une minute (mais notez que ce n'est pas une réponse Ring valide!).
Comme il ressort de cet exemple, il example-route
s'agit simplement d'une fonction, et très simple en plus; il examine la demande, détermine s'il est intéressé à la traiter (en examinant :request-method
et :uri
) et, si c'est le cas, renvoie une carte de réponse de base.
Ce qui est également évident, c'est que le corps de l'itinéraire n'a pas vraiment besoin d'être évalué à une carte de réponse appropriée; Compojure fournit une gestion par défaut sensée pour les chaînes (comme vu ci-dessus) et un certain nombre d'autres types d'objets; voir la compojure.response/render
multiméthode pour plus de détails (le code est entièrement auto-documenté ici).
Essayons d'utiliser defroutes
maintenant:
(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))
Les réponses à l'exemple de requête affiché ci-dessus et à sa variante avec :request-method :head
sont comme prévu.
Le fonctionnement interne de example-routes
est tel que chaque voie est essayée à son tour; dès que l'un d'eux retourne une non- nil
réponse, cette réponse devient la valeur de retour de l'ensemble du example-routes
gestionnaire. Pour plus de commodité, les defroutes
gestionnaires -defined sont encapsulés wrap-params
et wrap-cookies
implicitement.
Voici un exemple d'itinéraire plus complexe:
(def echo-typed-url-route
(GET "*" {:keys [scheme server-name server-port uri]}
(str (name scheme) "://" server-name ":" server-port uri)))
Notez la forme de déstructuration à la place du vecteur vide précédemment utilisé. L'idée de base ici est que le corps de l'itinéraire pourrait être intéressé par certaines informations sur la demande; comme cela arrive toujours sous la forme d'une carte, une forme de déstructuration associative peut être fournie pour extraire les informations de la requête et les lier à des variables locales qui seront dans la portée du corps de l'itinéraire.
Un test de ce qui précède:
user> (echo-typed-url-route {:server-port 80
:server-name "127.0.0.1"
:remote-addr "127.0.0.1"
:uri "/foo/bar"
:scheme :http
:headers {}
:request-method :get})
{:status 200,
:headers {"Content-Type" "text/html"},
:body "http://127.0.0.1:80/foo/bar"}
L'idée géniale de suivi de ce qui précède est que des itinéraires plus complexes peuvent assoc
fournir des informations supplémentaires sur la demande au stade de la correspondance:
(def echo-first-path-component-route
(GET "/:fst/*" [fst] fst))
Cela répond à une :body
de "foo"
la demande de l'exemple précédent.
Deux choses sont nouvelles dans ce dernier exemple: le "/:fst/*"
et le vecteur de liaison non vide [fst]
. Le premier est la syntaxe de type Rails-and-Sinatra susmentionnée pour les modèles d'URI. C'est un peu plus sophistiqué que ce qui ressort de l'exemple ci-dessus en ce que les contraintes de regex sur les segments d'URI sont supportées (par exemple ["/:fst/*" :fst #"[0-9]+"]
peuvent être fournies pour que la route n'accepte que les valeurs à tous les chiffres :fst
ci-dessus). Le second est une manière simplifiée de faire correspondre l' :params
entrée dans la mappe de demande, qui est elle-même une mappe; il est utile pour extraire des segments URI de la requête, des paramètres de chaîne de requête et des paramètres de formulaire. Un exemple pour illustrer ce dernier point:
(defroutes echo-params
(GET "/" [& more]
(str more)))
user> (echo-params
{:server-port 80
:server-name "127.0.0.1"
:remote-addr "127.0.0.1"
:uri "/"
:query-string "foo=1"
:scheme :http
:headers {}
:request-method :get})
{:status 200,
:headers {"Content-Type" "text/html"},
:body "{\"foo\" \"1\"}"}
Ce serait le bon moment pour jeter un œil à l'exemple du texte de la question:
(defroutes main-routes
(GET "/" [] (workbench))
(POST "/save" {form-params :form-params} (str form-params))
(GET "/test" [& more] (str "<pre>" more "</pre>"))
(GET ["/:filename" :filename #".*"] [filename]
(response/file-response filename {:root "./static"}))
(ANY "*" [] "<h1>Page not found.</h1>"))
Analysons chaque itinéraire tour à tour:
(GET "/" [] (workbench))
- lors du traitement d'une GET
requête avec :uri "/"
, appelez la fonction workbench
et restituez tout ce qu'elle renvoie dans une carte de réponse. (Rappelez-vous que la valeur de retour peut être une carte, mais aussi une chaîne, etc.)
(POST "/save" {form-params :form-params} (str form-params))
- :form-params
est une entrée dans la table des requêtes fournie par le wrap-params
middleware (rappelons qu'elle est implicitement incluse par defroutes
). La réponse sera la norme {:status 200 :headers {"Content-Type" "text/html"} :body ...}
avec (str form-params)
substitué ...
. (Un POST
gestionnaire un peu inhabituel , ce ...)
(GET "/test" [& more] (str "<pre> more "</pre>"))
- cela ferait par exemple écho à la représentation sous forme de chaîne de la carte {"foo" "1"}
si l'agent utilisateur le demandait "/test?foo=1"
.
(GET ["/:filename" :filename #".*"] [filename] ...)
- la :filename #".*"
partie ne fait rien du tout (puisque #".*"
correspond toujours). Il appelle la fonction utilitaire Ring ring.util.response/file-response
pour produire sa réponse; la {:root "./static"}
partie lui indique où chercher le fichier.
(ANY "*" [] ...)
- un itinéraire fourre-tout. Il est conseillé à Compojure de toujours inclure une telle route à la fin d'un defroutes
formulaire pour s'assurer que le gestionnaire en cours de définition renvoie toujours une carte de réponse Ring valide (rappelez-vous qu'un échec de correspondance d'itinéraire entraîne nil
).
Pourquoi de cette façon?
L'un des objectifs du middleware Ring est d'ajouter des informations à la carte des requêtes; ainsi le middleware de gestion des cookies ajoute une :cookies
clé à la requête,wrap-params
ajoute :query-params
et / ou:form-params
si une chaîne de requête / des données de formulaire sont présentes et ainsi de suite. (Strictement parlant, toutes les informations que les fonctions middleware ajoutent doivent déjà être présentes dans la mappe de requêtes, car c'est ce qu'elles sont transmises; leur travail consiste à les transformer pour qu'il soit plus pratique de travailler avec les gestionnaires qu'elles enveloppent.) Finalement, la demande "enrichie" est transmise au gestionnaire de base, qui examine la carte de demande avec toutes les informations bien prétraitées ajoutées par le middleware et produit une réponse. (L'intergiciel peut faire des choses plus complexes que cela - comme envelopper plusieurs gestionnaires "internes" et choisir entre eux, décider d'appeler ou non le ou les gestionnaires encapsulés du tout, etc. Cela sort cependant du cadre de cette réponse.)
Le gestionnaire de base, à son tour, est généralement (dans des cas non triviaux) une fonction qui a tendance à ne nécessiter qu'une poignée d'informations sur la requête. (Par exemple, ring.util.response/file-response
ne se soucie pas de la plupart de la requête; il n'a besoin que d'un nom de fichier.) D'où la nécessité d'un moyen simple d'extraire uniquement les parties pertinentes d'une requête Ring. Compojure vise à fournir un moteur de correspondance de modèles à usage spécial, pour ainsi dire, qui fait exactement cela.