DEV Community

Guilherme Rodrigues de Melo
Guilherme Rodrigues de Melo

Posted on

Clojure na Web com Ring

Ring é uma biblioteca baseada no WSGI do Python e no Rack do Ruby que nos ajuda na criação de aplicações web em Clojure, abstraindo os detalhes do HTTP. Ring é a base para muitas aplicações e bibliotecas do mundo web em Clojure, pois ele trabalha com o baixo nível das requisições HTTP. Ring possui três conceitos principais: Handler, Adapter e Middleware.

Handler

Para trabalhar com as requisições, precisamos criar handlers, que nada mais são do que funções que recebem uma requisição e devolvem uma resposta. As requisições e as repostas são mapas que contém informações como chaves baseadas na API de Servlet.

Requisição

Uma requisição contém informações que são acessadas através das chaves, onde as principais são:

  • :server-port: Porta do servidor que está recebendo a requisição
  • :server-name: Nome ou IP do servidor que está recebendo a requisição
  • :query-string: Query string da requisição (?q=name)
  • :request-method: Método HTTP utilizado na requisição (:post, :get, etc)
  • :body: Corpo da requisição
  • :headers: Headers da requisição

Resposta

Uma resposta contém apenas três informações que são:

  • :body: Corpo da resposta
  • :headers: Header da resposta contendo um mapa com chave/valor
  • :status: Status da resposta (200, 404, 500, etc)

Adapter

Um adapter é uma ponte entre a aplicação Ring e os detalhes de implementação do protocolo HTTP. O adapter recebe uma requisição, transforma a requisição em um mapa e passa esse mapa para a aplicação Ring realizar o processamento. Após processar o mapa recebido, a aplicação Ring retorna um mapa de resposta que é usado pelo adapter para retornar a resposta HTTP.

Alguns exemplos de adapter:

Middleware

Um middleware permite que alteremos a forma como a requisição é processada. Middlewares são utilizados para ampliar a funcionalidade dos handlers. Um middleware é uma função que recebe um handler e outros parâmetros opcionais, processa-os e retornando um novo handler com novos comportamentos. No exemplo a seguir veremos como criar e utilizar middlewares existentes.

Criando um projeto com Ring

Para criar um servidor com o Ring, precisamos primeiramente criar um projeto. No nosso caso, utilizaremos o Leiningen para a criação do projeto. Para instalar e configurar o Leiningen, acesse o site e siga as instruções. Após instalar o Leiningen, execute o seguinte comando no seu terminal:

*lein new hello-ring

Será gerado um projeto com a seguinte estrutura:

Alt Text

Diretórios mais importantes para nós nesse momento:

  • doc: Diretório onde é colocada a documentação do projeto
  • src: Diretório onde fica o código fonte do projeto
  • test: Diretório onde ficam os testes
  • project.clj: Arquivo responsável por toda configuração do projeto (semelhante ao pom.xml de projetos Java com Maven)

Abra o arquivo project.clj. Começaremos adicionando novas dependências para o projeto. No vetor :dependencies, adicione duas dependências do Ring:

:dependencies [[org.clojure/clojure "1.10.0"]
               ; principais funções do Ring
               [ring/ring-core "1.8.1"]
               ; adapter Jetty para Ring
               [ring/ring-jety-adapter "1.8.1"]]
Enter fullscreen mode Exit fullscreen mode

Após adicionar as duas dependências, execute o comando para baixa-las no seu reposítorio local do Leiningen.

*lein deps

Agora, abra o arquivo core.clj que está dentro do diretório src/hello_ring. Nesse arquivo temos a declaração do namespace hello-ring.core e a importação do adapter do servidor Jetty.

(ns hello-ring.core
  (:require [ring.adapter.jetty :as jetty]))
Enter fullscreen mode Exit fullscreen mode

Vamos adicionar nosso primeiro handler que retornará um mapa como resposta contendo o status, o corpo da requisição e um header de Content-Type.

(defn ola-web [request]
  {:status 200
   :body "Olá web"
   :headers {"Content-Type" "text/plain; charset=utf-8"}})
Enter fullscreen mode Exit fullscreen mode

Por fim, criamos uma função chamada -main que será chamada pelo Leiningen. Dentro da função main, iniciamos o servidor Jetty associando a ele o handler (ola-web) e um mapa com a configuração da porta (3000).

(ns hello-ring.core
  (:require [ring.adapter.jetty :as jetty]))

(defn ola-web [request]
  {:status 200
   :body "Olá web"
   :headers {"Content-Type" "text/plain; charset=utf-8"}})

(defn -main [& args]
  (jetty/run-jetty ola-web {:port 3000}))
Enter fullscreen mode Exit fullscreen mode

Para inicializar o servidor pelo terminal, precisamos fazer uma configuração a mais. Abra o arquivo project.clj e adicione uma linha (:main hello-ring.core) para dizer ao Leiningen que o projeto inicializará pelo namespace hello-ring.core através da função -main:

(defproject hello-ring "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.0"]
                 [ring/ring-core "1.8.1"]
                 [ring/ring-jetty-adapter "1.8.1"]]
  :main hello-ring.core
  :repl-options {:init-ns hello-ring.core})
Enter fullscreen mode Exit fullscreen mode

Agora, abra novamente o terminal e execute o comando para incializar o projeto.

*lein run

O servidor será inicializado. Abra o navegador e digite localhost:3000. Deve ser mostrada a mensagem "Olá web".

Criando um middleware

Criaremos um middleware que adicionará a chave :query-params ao header da requisição com os dados da query-string.

(defn parse-query
  "Transforma a query-string em um mapa de parâmetros"
  [query-string]
  (if (> (count query-string) 0)
    (apply hash-map (str/split query-string #"[&=]"))))

(defn wrap-query-param
  "Adiciona a chave :query-param ao header da requisição"
  [handler]
  (fn [request]
    (let [query-param (parse-query (:query-string request))
          nova-req (assoc request :query-params query-param)]
      (handler nova-req))))
Enter fullscreen mode Exit fullscreen mode

No código acima, criamos a função parse-query responsável por quebrar os parâmetros da requisição e criar um mapa de chave/valor. Nosso middleware (wrap-query-param) captura os parâmetros, recebe o mapa criado pela função anterior e associa esse mapa a requisição atual, criando uma nova requisição e passando-a para o handler recebido como parâmetro do middleware.

Para utilizar nosso middleware, passamos nosso handler como parâmetro para ele, que por sua vez, é passado como parâmetro para a função que cria o servidor.

(defn -main [& args]
  (jetty/run-jetty (wrap-query-param ola-web) {:port 3000}))
Enter fullscreen mode Exit fullscreen mode

Alteramos o código da função ola-web para pegar o parâmetro e retornar a mensagem. Abaixo o código completo:

(ns hello-ring.core
  (:require [ring.adapter.jetty :as jetty]
            [clojure.string :as str]))

(defn ola-web [request]
  (let [nome (get (:query-params request) "nome")]
    {:status 200
     :body (str "Olá " nome)
     :headers {"Content-Type" "text/plain; charset=utf-8"}}))

(defn parse-query
  "Transforma a query-string em um mapa de parâmetros"
  [query-string]
  (if (> (count query-string) 0)
    (apply hash-map (str/split query-string #"[&=]"))))

(defn wrap-query-param
  "Adiciona a chave :query-param ao header da requisição"
  [handler]
  (fn [request]
    (let [query-param (parse-query (:query-string request))
          nova-req (assoc request :query-params query-param)]
      (handler nova-req))))

(defn -main [& args]
  (jetty/run-jetty (wrap-query-param ola-web) {:port 3000}))
Enter fullscreen mode Exit fullscreen mode

Execute novamente o comando para subir o servidor:

*lein run

No navegador, digite localhost:3000/?nome=Guilherme e veja o resultado.

Utilizando um middleware pronto para retornar JSON

Agora utilizaremos dois middlewares existentes. Um vai capturar os dados da query-string como fizemos no nosso middleware e o outro retornará os dados da requisição como JSON. Abra o arquivo project.clj e adicione a seguinte dependência responsável pelo middleware de JSON:

*[ring/ring-json "0.5.0"]

No nosso namespace hello-ring.core, vamos adicionar as dependências dos dois middlewares, sendo o wrap-params pertencente à biblioteca ring-core e o wrap-json-response à biblioteca ring-json.

(ns hello-ring.core
  (:require [ring.adapter.jetty :as jetty]
            [ring.middleware.params :refer [wrap-params]]
            [ring.middleware.json :refer [wrap-json-response]]))

(defn ola-web [request]
  (let [nome (get (:query-params request) "nome")]
    {:status 200
     :body {:nome nome}
     :headers {}}))

(defn -main [& args]
  (jetty/run-jetty
    (-> ola-web
        wrap-params
        wrap-json-response)
    {:port 3000}))
Enter fullscreen mode Exit fullscreen mode

No nosso código acima temos algo um pouco diferente na inicialização do servidor. Utilizamos uma thread-fist para processar nosso handler utilizando os dois middlewares. Thread-first é uma forma que temos em Clojure de processar informações de forma clara e encadeada, pois o resultado da primeira linha é passado como parâmetro para a função da linha de baixo. Nesse caso, passamos nosso handler como parâmetro para o middleware wrap-params e o resultado do retorno desse middleware é passado como parâmetro para o middleware de baixo (wrap-json-response).

Execute novamente o comando para subir o servidor:

*lein run

No navegador, digite localhost:3000/?nome=Guilherme. O resultado é mostrado no formato JSON.

Podemos melhorar o retorno do nosso handler adicionando uma biblioteca para utilizar os métodos HTTP. No arquivo project.clj, adicione a dependência abaixo:

*[metosin/ring-http-response "0.9.1"]

No namespace hello-ring.core, adicione a dependência do http-response para a utilização da função ok. Passamos o mesmo mapa para a função ok, que retornará o status 200 juntamente com o corpo da resposta.

(ns hello-ring.core
  (:require [ring.adapter.jetty :as jetty]
            [ring.middleware.params :refer [wrap-params]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.util.http-response :refer [ok]]))

(defn ola-web [request]
  (let [nome (get (:query-params request) "nome")]
    (ok {:nome nome})))

(defn -main [& args]
  (jetty/run-jetty
    (-> ola-web
        wrap-params
        wrap-json-response)
    {:port 3000}))
Enter fullscreen mode Exit fullscreen mode

Ring é uma biblioteca potente e básica para inicializar o desenvolvimento web em Clojure, porém é a base para outras bibliotecas que veremos mais pra frente, como Compojure, Reitit e Pedestal. Nos vemos num próximo post. :)

Top comments (0)