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.cljBem 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-methodfosse :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-methode: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 defroutesagora:
(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 nilresposta, essa resposta se tornará o valor de retorno de todo o example-routesmanipulador. Como uma conveniência adicional, defroutesmanipuladores definidos são incluídos wrap-paramse wrap-cookiesimplicitamente.
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 assocextrair 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 :bodyde "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 :fstdo acima). A segunda é uma maneira simplificada de correspondência na :paramsentrada 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 GETsolicitação :uri "/", chame a função workbenche 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-paramsmiddleware (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 POSTmanipulador 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-responsepara 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 defroutesformulá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 :cookieschave à solicitação, wrap-paramsadiciona :query-paramse / ou:form-paramsse 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-responsenã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.