Explicação de Compojure (até certo ponto)
NB. Estou trabalhando com o Compojure 0.4.1 ( aqui está o commit da versão 0.4.1 no GitHub).
Por quê?
compojure/core.clj
Bem no topo , há este resumo útil do propósito do Compojure:
Uma sintaxe concisa para gerar manipuladores de anel.
Em um nível superficial, isso é tudo que há para a pergunta "por quê". Para ir um pouco mais fundo, vamos dar uma olhada em como funciona um aplicativo estilo Ring:
Uma solicitação chega e é transformada em um mapa Clojure de acordo com as especificações do Anel.
Este mapa é afunilado em uma chamada "função de manipulador", que deve produzir uma resposta (que também é um mapa Clojure).
O mapa de resposta é transformado em uma resposta HTTP real e enviado de volta ao cliente.
A etapa 2. acima é a mais interessante, pois é responsabilidade do manipulador examinar o URI usado na solicitação, examinar quaisquer cookies etc. e, por fim, chegar a uma resposta apropriada. Obviamente, é necessário que todo esse trabalho seja fatorado em uma coleção de peças bem definidas; essas são normalmente uma função de manipulador "base" e uma coleção de funções de middleware que a envolvem. O objetivo do Compojure é simplificar a geração da função de manipulador base.
Quão?
O Compojure é construído em torno da noção de "rotas". Na verdade, eles são implementados em um nível mais profundo pelo biblioteca Clout (um desdobramento do projeto Compojure - muitas coisas foram movidas para bibliotecas separadas na transição 0.3.x -> 0.4.x). Uma rota é definida por (1) um método HTTP (GET, PUT, HEAD ...), (2) um padrão URI (especificado com sintaxe que será aparentemente familiar para Webby Rubyists), (3) uma forma de desestruturação usada em vincular partes do mapa de solicitação a nomes disponíveis no corpo, (4) um corpo de expressões que precisa produzir uma resposta de Ring válida (em casos não triviais, isso geralmente é apenas uma chamada para uma função separada).
Este pode ser um bom ponto para dar uma olhada em um exemplo simples:
(def example-route (GET "/" [] "<html>...</html>"))
Vamos testar isso no REPL (o mapa de solicitação abaixo é o mapa de solicitação de anel válido mínimo):
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>"}
Se :request-method
fosse :head
, a resposta seria nil
. Voltaremos à questão do quenil
significa aqui em um minuto (mas observe que não é uma resposta de anel válida!).
Como fica claro neste exemplo, example-route
é apenas uma função, e muito simples; olha a solicitação, determina se está interessado em lidar com ela (examinando :request-method
e:uri
) e, em caso afirmativo, retorna um mapa de resposta básico.
O que também é evidente é que o corpo da rota não precisa realmente ser avaliado para um mapa de resposta adequado; O Compojure fornece tratamento padrão lógico para strings (como visto acima) e uma série de outros tipos de objetos; Veja ocompojure.response/render
multimétodo para obter detalhes (o código é inteiramente autodocumentado aqui).
Vamos tentar usar defroutes
agora:
(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))
As respostas à solicitação de exemplo exibida acima e à sua variante com :request-method :head
são as esperadas.
O funcionamento interno do example-routes
é tal que cada rota é tentada separadamente; assim que um deles retornar uma não nil
resposta, essa resposta se tornará o valor de retorno de todo o example-routes
manipulador. Como uma conveniência adicional, defroutes
manipuladores definidos são incluídos wrap-params
e wrap-cookies
implicitamente.
Aqui está um exemplo de uma rota mais complexa:
(def echo-typed-url-route
(GET "*" {:keys [scheme server-name server-port uri]}
(str (name scheme) "://" server-name ":" server-port uri)))
Observe a forma de desestruturação no lugar do vetor vazio usado anteriormente. A ideia básica aqui é que o corpo da rota pode estar interessado em algumas informações sobre a solicitação; uma vez que sempre chega na forma de um mapa, um formulário de desestruturação associativa pode ser fornecido para extrair informações da solicitação e vinculá-la às variáveis locais que estarão no escopo do corpo da rota.
Um teste do acima:
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"}
A ideia brilhante de acompanhamento do acima é que as rotas mais complexas podem assoc
extrair informações sobre a solicitação no estágio de correspondência:
(def echo-first-path-component-route
(GET "/:fst/*" [fst] fst))
Isso responde com um :body
de "foo"
à solicitação do exemplo anterior.
Duas coisas são novas sobre este último exemplo: o "/:fst/*"
e o vetor de ligação não vazio [fst]
. O primeiro é a sintaxe do tipo Rails e Sinatra mencionada anteriormente para padrões de URI. É um pouco mais sofisticado do que o que é aparente no exemplo acima, pois as restrições regex em segmentos de URI são suportadas (por exemplo, ["/:fst/*" :fst #"[0-9]+"]
podem ser fornecidas para fazer a rota aceitar apenas valores de todos os dígitos :fst
do acima). A segunda é uma maneira simplificada de correspondência na :params
entrada no mapa de solicitação, que é um mapa; é útil para extrair segmentos de URI da solicitação, parâmetros de string de consulta e parâmetros de formulário. Um exemplo para ilustrar o último ponto:
(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\"}"}
Este seria um bom momento para dar uma olhada no exemplo do texto da pergunta:
(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>"))
Vamos analisar cada rota por vez:
(GET "/" [] (workbench))
- ao lidar com uma GET
solicitação :uri "/"
, chame a função workbench
e renderize tudo o que ela retornar em um mapa de resposta. (Lembre-se de que o valor de retorno pode ser um mapa, mas também uma string etc.)
(POST "/save" {form-params :form-params} (str form-params))
- :form-params
é uma entrada no mapa de solicitação fornecido pelo wrap-params
middleware (lembre-se de que está implicitamente incluído por defroutes
). A resposta será o padrão {:status 200 :headers {"Content-Type" "text/html"} :body ...}
com (str form-params)
substituído por ...
. (Um POST
manipulador um pouco incomum , este ...)
(GET "/test" [& more] (str "<pre> more "</pre>"))
- isto iria, por exemplo, ecoar de volta a representação da string do mapa {"foo" "1"}
se o agente do usuário solicitasse "/test?foo=1"
.
(GET ["/:filename" :filename #".*"] [filename] ...)
- a :filename #".*"
parte não faz nada (uma vez que #".*"
sempre corresponde). Ele chama a função de utilidade Ring ring.util.response/file-response
para produzir sua resposta; a {:root "./static"}
parte informa onde procurar o arquivo.
(ANY "*" [] ...)
- uma rota abrangente. É uma boa prática de Compojure sempre incluir essa rota no final de um defroutes
formulário para garantir que o manipulador sendo definido sempre retorne um mapa de resposta de anel válido (lembre-se de que uma falha de correspondência de rota resulta em nil
).
Por que assim?
Um dos objetivos do middleware Ring é adicionar informações ao mapa de solicitação; assim, o middleware de manipulação de cookies adiciona uma :cookies
chave à solicitação, wrap-params
adiciona :query-params
e / ou:form-params
se uma string de consulta / dados de formulário estiverem presentes e assim por diante. (Estritamente falando, todas as informações que as funções de middleware estão adicionando já devem estar presentes no mapa de solicitação, uma vez que é isso que elas passam; seu trabalho é transformá-lo para ser mais conveniente para trabalhar com os manipuladores que envolvem.) Por fim, a solicitação "enriquecida" é passada ao manipulador de base, que examina o mapa da solicitação com todas as informações bem pré-processadas adicionadas pelo middleware e produz uma resposta. (Middleware pode fazer coisas mais complexas do que isso - como empacotar vários manipuladores "internos" e escolher entre eles, decidir se deve chamar o (s) manipulador (es) empacotados etc. Isso está, no entanto, fora do escopo desta resposta.)
O manipulador básico, por sua vez, é geralmente (em casos não triviais) uma função que tende a precisar de apenas alguns itens de informação sobre a solicitação. (Por exemplo, ring.util.response/file-response
não se preocupa com a maior parte da solicitação; ele só precisa de um nome de arquivo.) Daí a necessidade de uma maneira simples de extrair apenas as partes relevantes de uma solicitação de Ring. O Compojure visa fornecer um mecanismo de correspondência de padrões de propósito especial, por assim dizer, que faz exatamente isso.