Clojure na Web com Ring
Tags: clojure, ring, web
Published: 2021-01-08 18:48:45

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:

* [Jetty](https://github.com/ring-clojure/ring/tree/master/ring-jetty-adapter)
* [Apache HttpCore](https://github.com/mmcgrana/ring-httpcore-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 ](https://leiningen.org/) 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](https://dev-to-uploads.s3.amazonaws.com/i/p5ti066tf1at7rtvdiva.png)

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"]]
```

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]))
```

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"}})
```

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}))
```
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})
```
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))))
```

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}))
```
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}))
```
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}))
```
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}))
```

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