Qual é a “grande ideia” por trás das rotas de compojura?

109

Sou novo no Clojure e tenho usado o Compojure para escrever um aplicativo web básico. No defroutesentanto, estou atingindo uma barreira com a sintaxe do Compojure e acho que preciso entender o "como" e o "porquê" por trás de tudo isso.

Parece que um aplicativo estilo Ring começa com um mapa de solicitação HTTP e, em seguida, apenas passa a solicitação por meio de uma série de funções de middleware até que seja transformada em um mapa de resposta, que é enviado de volta ao navegador. Este estilo parece muito "baixo nível" para desenvolvedores, daí a necessidade de uma ferramenta como o Compojure. Eu posso ver essa necessidade de mais abstrações em outros ecossistemas de software também, principalmente com o WSGI do Python.

O problema é que não entendo a abordagem de Compojure. Vamos pegar a seguinte defroutesexpressão S:

(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>"))

Eu sei que a chave para entender tudo isso está em algum macro vodu, mas não entendo totalmente macros (ainda). Eu encarei a defroutesfonte por um longo tempo, mas simplesmente não entendo! O que está acontecendo aqui? Compreender a "grande ideia" provavelmente me ajudará a responder a estas perguntas específicas:

  1. Como faço para acessar o ambiente de anel de dentro de uma função roteada (por exemplo, a workbenchfunção)? Por exemplo, digamos que eu queira acessar os cabeçalhos HTTP_ACCEPT ou alguma outra parte da solicitação / middleware.
  2. Qual é o problema com a desestruturação ( {form-params :form-params})? Quais palavras-chave estão disponíveis para mim durante a desestruturação?

Eu realmente gosto de Clojure, mas estou tão perplexo!

Sean Woods
fonte

Respostas:

212

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:

  1. Uma solicitação chega e é transformada em um mapa Clojure de acordo com as especificações do Anel.

  2. Este mapa é afunilado em uma chamada "função de manipulador", que deve produzir uma resposta (que também é um mapa Clojure).

  3. 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:

  1. (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.)

  2. (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 ...)

  3. (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".

  4. (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.

  5. (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.

Michał Marczyk
fonte
3
"Como uma conveniência adicional, manipuladores definidos por defroutes são envolvidos em parâmetros e cookies de embrulho implicitamente." - A partir da versão 0.6.0, você deve adicioná-los explicitamente. Ref. Github.com/weavejester/compojure/commit/…
Dan Midwood
3
Muito bem colocado. Essa resposta deve estar na página inicial do Compojure.
Siddhartha Reddy
2
Leitura obrigatória para qualquer novo no Compojure. Desejo que cada wiki e postagem de blog sobre o tópico comece com um link para isso.
jemmons
7

Há um excelente artigo em booleanknot.com de James Reeves (autor de Compojure), e lê-lo fez "clicar" para mim, então eu retranscrevi parte dele aqui (na verdade, foi tudo o que fiz).

Há também um slidedeck aqui do mesmo autor , que responde exatamente a essa pergunta.

O Compojure é baseado no Ring , que é uma abstração para solicitações http.

A concise syntax for generating Ring handlers.

Então, quais são esses manipuladores de anel ? Extraia do documento:

;; Handlers are functions that define your web application.
;; They take one argument, a map representing a HTTP request,
;; and return a map representing the HTTP response.

;; Let's take a look at an example:

(defn what-is-my-ip [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body (:remote-addr request)})

Bastante simples, mas também de baixo nível. O manipulador acima pode ser definido de forma mais concisa usando a ring/utilbiblioteca.

(use 'ring.util.response)

(defn handler [request]
  (response "Hello World"))

Agora queremos chamar manipuladores diferentes, dependendo da solicitação. Poderíamos fazer algum roteamento estático como:

(defn handler [request]
  (or
    (if (= (:uri request) "/a") (response "Alpha"))
    (if (= (:uri request) "/b") (response "Beta"))))

E refatore assim:

(defn a-route [request]
  (if (= (:uri request) "/a") (response "Alpha")))

(defn b-route [request]
  (if (= (:uri request) "/b") (response "Beta"))))

(defn handler [request]
  (or (a-route request)
      (b-route request)))

O interessante que James observa então é que isso permite o aninhamento de rotas, porque "o resultado da combinação de duas ou mais rotas é em si mesmo uma rota".

(defn ab-routes [request]
  (or (a-route request)
      (b-route request)))

(defn cd-routes [request]
  (or (c-route request)
      (d-route request)))

(defn handler [request]
  (or (ab-routes request)
      (cd-routes request)))

Agora, estamos começando a ver algum código que parece que poderia ser fatorado, usando uma macro. O Compojure fornece uma defroutesmacro:

(defroutes ab-routes a-route b-route)

;; is identical to

(def ab-routes (routes a-route b-route))

O Compojure fornece outras macros, como a GETmacro:

(GET "/a" [] "Alpha")

;; will expand to

(fn [request#]
  (if (and (= (:request-method request#) ~http-method)
           (= (:uri request#) ~uri))
    (let [~bindings request#]
      ~@body)))

Essa última função gerada se parece com o nosso manipulador!

Por favor, certifique-se de verificar a postagem de James , pois ela contém explicações mais detalhadas.

nha
fonte
4

Para quem ainda lutou para descobrir o que está acontecendo com as rotas, pode ser que, como eu, você não entenda a ideia de desestruturação.

Na verdade, a leitura dos documentoslet ajudou a esclarecer todo o "de onde vêm os valores mágicos?" questão.

Estou colando as seções relevantes abaixo:

Clojure oferece suporte a vinculação estrutural abstrata, frequentemente chamada de desestruturação, em listas de vinculação let, listas de parâmetros fn e qualquer macro que se expande em let ou fn. A ideia básica é que um formulário de ligação pode ser um literal de estrutura de dados contendo símbolos que são vinculados às respectivas partes do init-expr. A vinculação é abstrata no sentido de que um literal de vetor pode se vincular a qualquer coisa que seja sequencial, enquanto um literal de mapa pode se vincular a qualquer coisa que seja associativa.

Vector binding-exprs permite vincular nomes a partes de coisas sequenciais (não apenas vetores), como vetores, listas, seqs, strings, arrays e qualquer coisa que suporte nth. A forma sequencial básica é um vetor de formas de ligação, que será ligada a elementos sucessivos de init-expr, pesquisados ​​via nth. Além disso, e opcionalmente, & seguido por formas de ligação fará com que essa forma de ligação seja ligada ao restante da sequência, ou seja, aquela parte ainda não ligada, pesquisada via nthnext. Finalmente, também opcional,: seguido por um símbolo fará com que esse símbolo seja vinculado a todo o init-expr:

(let [[a b c & d :as e] [1 2 3 4 5 6 7]]
  [a b c d e])
->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]]

Vector binding-exprs permite vincular nomes a partes de coisas sequenciais (não apenas vetores), como vetores, listas, seqs, strings, arrays e qualquer coisa que suporte nth. A forma sequencial básica é um vetor de formas de ligação, que será ligada a elementos sucessivos de init-expr, pesquisados ​​via nth. Além disso, e opcionalmente, & seguido por formas de ligação fará com que essa forma de ligação seja ligada ao restante da sequência, ou seja, aquela parte ainda não ligada, pesquisada via nthnext. Finalmente, também opcional,: seguido por um símbolo fará com que esse símbolo seja vinculado a todo o init-expr:

(let [[a b c & d :as e] [1 2 3 4 5 6 7]]
  [a b c d e])
->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]]
Raça Pieter
fonte
3

Eu ainda não comecei no clojure web stuff, mas, vou, aqui estão as coisas que marquei.

Nickik
fonte
Obrigado, esses links são definitivamente úteis. Venho trabalhando nesse problema a maior parte do dia e estou em um lugar melhor com ele ... Vou tentar postar um follow-up em algum momento.
Sean Woods de
1

Qual é o problema com a desestruturação ({form-params: form-params})? Quais palavras-chave estão disponíveis para mim durante a desestruturação?

As chaves disponíveis são aquelas que estão no mapa de entrada. A desestruturação está disponível dentro dos formulários let e doseq, ou dentro dos parâmetros para fn ou defn

Esperamos que o código a seguir seja informativo:

(let [{a :thing-a
       c :thing-c :as things} {:thing-a 0
                               :thing-b 1
                               :thing-c 2}]
  [a c (keys things)])

=> [0 2 (:thing-b :thing-a :thing-c)]

um exemplo mais avançado, mostrando a desestruturação aninhada:

user> (let [{thing-id :id
             {thing-color :color :as props} :properties} {:id 1
                                                          :properties {:shape
                                                                       "square"
                                                                       :color
                                                                       0xffffff}}]
            [thing-id thing-color (keys props)])
=> [1 16777215 (:color :shape)]

Quando usado com sabedoria, a desestruturação organiza seu código, evitando o acesso aos dados padronizados. usando: as e imprimindo o resultado (ou as chaves do resultado) você pode ter uma ideia melhor de quais outros dados você pode acessar.

ferreiro
fonte