Codemountain, Paulo Suzart's Blog

Clojure na web – Compojure/Hiccup/Sandbar/Leiningen

with one comment

Quanta coisa aconteceu desde o último post! Umas boas, outras ruins. Uma das boas é que agora você pode ler artigos meus em scala-br.org. O primeiro post na verdade é um repost de um texto já publicado aqui no Codemountain.

Desde o último post fiquei bastante ocupado com a pós, até passei a estudar menos as coisas que gosto por conta disso. ¬¬ Mas arrumei um tempo pra parar de apenas dar RT no twitter sobre clojure e resolvi fazer uma micro app para web usando esta fabulosa linguagem e alguns dos frameworks disponíveis.

O post não contém nada extraordinário ou que não seja possível encontrar nas documentações dos frameworks, a intenção mesmo é fazer uma introdução – em português – do que pode ser seu ferramental de amanhã.

Anote a receita:

  • Um pouco de Ring. Algo com WSGI para Clojure, abstraindo o HTTP numa DSL concisa. Aqui vamos usar seu adaptador para Jetty.
  • Uma pitada de  Compojure. Um webframework simples onde o foco aqui  é o uso dos seus routes para construir o que o Ring chama de handlers.
  • Uma boa dose de Hiccup. Isto é, vamos representar nosso html em Clojure. Não é fantástico?
  • Uma forma em Sandbar. Na verdade é possível construir html forms com Compojure + Hiccup. Mas acredite, construção de forms com Sandbar é mais sexy.
  • E pra selar tudo, nossa ferramenta de build expressa na linguagem: o Leiningen. Este nome vem de um personagem que luta contra formigas na Amazônia. As formigas que esta ferramenta luta contra são os antigos builds.xml usando Ant. Nome criativo.

Me referi a receita, por que criar software é uma arte como cozinhar, já dizia Terrence Parr, ele é só o autor do ANTLR.

A aplicação: Moyure

Muito simples. Uma página principal – sem autenticação ou belezas visuais – com um link para a página de criação de um registro do seus Meet Ups! O nome Moyure não tem uma história tão interessante quanto a do Leiningen😦

Após incluir o registro, a aplicação deve voltar à página principal e mostrar a listagem do registro existentes com um link para edição em cada um deles.

O Banco de Dados: STM

STM (Software Transactional Memory) é, a grosso modo, transações em memória. Isso mesmo, sem uso de locks explícitos podemos efetuar transações em memória usando Clojure Refs. A linguagem oferece outras formas de isolamento/concorrência, mas me pareceu suficiente usar Refs. O objetivo de ter feito um pequeno conjunto de funções é permitir que você faça um clone da aplicação no git (link mais abaixo) e possa executa-la sem se preocupar em gerar schemas/massa de dados ou mesmo instalar uma base. Nosso banco se resume a:

(ns moyure.db)
(def id (ref 0))

(def db (ref {}))

(defn nextval [] (dosync (alter id inc)))

(defn insert-meet
    "Insert a new meet."
    [d] (dosync  (let [nid (nextval)]
          (alter db assoc nid (assoc d :id nid))  nid)))

(defn find
    "If id present, returns the given entry.  Otherwise, returnts all
    entries (the actual map of db)"
    ([] @db)
    ([id] (get @db id)))

Não desanime. O post está só começando. Resumindo, temos aqui duas Refs. Veja id e db sendo definidas com (ref), isto é, para “alterar estes valores” precisamos faze-lo dentro de uma transação. Para isso usamos (dosync) para estabelecer um escopo transacional e (alter) para efetuar a alteração. É possível envolver mais de uma variável em uma transação, e é o que temos aqui. As variáveis id e db estão envolvidas na mesma transação e serão alteradas com sucesso juntas, ou retornarão ao seu estado original se necessário.

Dito isto, precisei garantir que esse banco de dados de última geração🙂 se comportaria como desenhei. Pra isso usei o pacote clojure.test disponível na própria linguagem. Os testes são simples e autodescritivos. Eles tem mesmo a cara do RSpec. Veja:

(ns moyure.test.db
    (:use [moyure.db])
    (:use [clojure.test]))

(deftest db-t
    (testing "db"

         (testing "should return 1 in the first insertion and 2 in the second."
            (is (= (inc @id) (insert-meet {:title "test" :when "today"})))
            (is (= (inc @id) (insert-meet {:title "doc" :when "saturday"})))

            (testing "And and then nextval should return 2."
                (is (= (inc @id) (nextval)))))

         (testing "Should return all entries (2) with unspecified id."
            (is (not= 0 (count (find))))
         (testing "But should return one entry using id as argument."
            (is (not= nil (find (dec @id))))))))

Testes desse tipo fazem a diferença, numa linguagem dinãmica, ainda mais. Por que basta você ficar sem mexer numa parte do código por uma semana e já não vai lembrar tão bem das coisas. Os testes vão garantir a evolução do seu código sem medos.

Existem libs mais apropriadas para BDD como o Lazytest apontada pelo @alandipert. Vale conferir.

O Projeto

Quase esqueço! Precisamos configurar o nosso novo projeto (sei que você vai fazer um clone do github, mas fica a dica). Para criar um novo projeto com o Leiningen, precisamos de um project.clj com a descrição do projeto. Uma versão simples seria:


(defproject moyure "1.0.0-SNAPSHOT"  :description "Save your meet ups in the speed of clojure"
    :dependencies [[org.clojure/clojure "1.2.0"]
                            [org.clojure/clojure-contrib "1.2.0"]
                            [compojure "0.5.2"]
                            [ring/ring-jetty-adapter "0.3.1"]
                            [hiccup "0.2.7"]
                            [sandbar/sandbar "0.3.0-SNAPSHOT"]])

A aplicação foi construída com este super Banco de dados em db.clj (acima) e core.clj que contém tudo: layout, forms, rotas de requisição http, etc. Mas sempre será boa prática separar melhor as coisas. Pra efeito de exemplo, acho que é suficiente.

A primeira coisa a definir na nossa aplicação pode ser o conjunto de rotas, isto é, quais urls serão acessíveis pelo usuário e para que função cada url será mapeada. defroutes é a macro responsável por gerar nossas rotas:

 (defroutes app-routes
     (GET "/" [] (home))
     (meetup-form (fn [request form] (layout form)))
     (route/not-found (layout [:h2 "Page not found dude!"])))
 

Observe a macro GET, ela mapeia o verbo GET Http na url / sem parâmetros para a invocação da função home. Em seguida vemos meetup-form, não necessitamos gerar explicitamente uma rota com verbo e url pois estes dois elementos definidos no próprio form com a macro defform. Esta macro é quem faz toda mágica do Sandbar, e entender vai demandar um pouco de intimidade com clojure e macros.

Nossa última rota é a rota para tudo que não seja “/” nem “/meetup” – a url para a página de cadastro. Usamos not-found, uma rota que retorna 404 para o browser.

Parece um monte de coisa até aqui pra fazer algo tão simples assim. É, parece, mas é questão de costume.

Agora vamos à função home ela é quem vai fazer a consulta por todos os registros no banco de dados e exibir uma tabela com os resultados.

(defn show-all
    "Generates a html snipet for entries"
    [a]
    (for [[k v] a]
        (let [id (:id v)
             title (:title v)]
             [:tr
                [:td id] [:td title]
                [:td (link-to (str "/meetup/" id) "Edit")]])))

(defn home
    "The welcome screen"
     []
     (layout [:div
                    [:b "Hello Visitor"]
                    [:p (link-to "/meetup" "New MeetUp")]
                    (if-let [a (db/find)]
                       [:table (show-all a)])]))

Aqui entra o Hiccup, de fato as tags aqui não passam de vetores com uma :chave e um valor nativos Clojure, o tratamento disso acontece na função layout (a seguir). É possível passar atributos para o elemento html em quesão, css, eventos js, etc. Tudo direto do clojure. Note a função find do nosso banco sendo invocada. E pra facilitar a modularização, a geração das tr/td html é feita a função show-all. Esta última recebe algo como {1 {:id 1 :title “Consulting” :when “Saturday” :subject “Check some code!”}}. Note o link para a página de edição, que é: /meetup/{id}. Simples! Ah, outra coisa fabulosa é que você abre a tag e o fechamento do vetor dela já basta, o Hiccup faz a geração correta para você. Me lembra até HAML ou SCAML.

Agora vamos para a melhor parte. O form com Sandbar. Eu espero algum dia construir algo assim. Vejam:


(def m-label
      {:title "Title"
       :when "When"
       :subject "Subject"})

(forms/defform meetup-form "/meetup"
:fields [(forms/hidden :id)
                (forms/textfield :title)
                (forms/textfield :when {:size 10})
                (forms/textarea :subject)]
:load #(db/find %)
: on-cancel "/"
: on-success #(do (db/insert-meet  % )
                 (flash-put! :user-message [:p "Meet up saved, go tell your friends!"])
                 "/")
:properties m-label)

A grande sacada do Sandbar é através de :chaves fazer a definição do que o nosso form precisa. Vale a pena ver esta macro. Nosso forme de nome meetup-form é bem explicativo, creio. Basta ressaltar as chaves :load  e :on-sucess, que são quase intuitivas, você só precisa saber o que é este % nelas.

Estas chaves representarão funções que recebem um argumento. :load representa uma função anônima (lembre-se do #) contendo o id da requisição em caso de edição. Ou seja, o valor de % é mapeado automaticamente para 1 na url “/meetup/1” . Então, passamos este valor para a função find do nosso banco. Esta função retorna um map com chaves de mesmo nome dos campos, permitindo que o formulário seja preenchido automaticamente. WOW!

E quanto a : on-success? Neste caso o % representa o mapa de campos preenchidos (estes campos podem ser validados com o próprio Sandbar, mas acho que já deu no post). Veja que a função anônima de : on-success  invoca a função de inseção com o mapa de valores, em seguida salva no scopo flash da requisição uma mensagem, e por último retorna para onde a navegação deve seguir após o dado ser persistido. Isso mesmo, tão simples quanto isso.

Não tem nada de ocultismo no %, esta á a forma curta de referenciar o primeiro argumento de uma função anônima em clojure. E o valor que ele recebe pode ser facilmente verificável na documentação da lib Sandbar.

Se você chegou até aqui, obrigado. Confesso que as vezes me sinto só nessa mundo Scala, Clojure, etc. Então não deixe de me seguir no twitter (@paulosuzart).

Por último, e tão importante quanto tudo. Está nossa função layout, ela é quem invoca a função html do Hiccup e gera o nosso correto HTML


(defn layout
    "Acts as a template wrapping all the content (cont) in the
base structure"
    [con]
    (html [:html
             [:head
                 [:title "Organize your meet ups with Moyure"]
                 (stylesheet "sandbar-forms.css")
                 (stylesheet "sandbar.css")]
             [:body
                 [:h2 "MOYURE"]
                 (if-let [m (flash-get :user-message)]
                     [:div m])
                  con]]))

Sempre que alguém invocar layout aqueles vectors contendo :chaves serão passados como parâmetro, e aqui acontece a mágica do HTML. Nesta função vemos o uso de .css – que peguei emprestado do pessoal do Sandbar (Não foram alterados e os créditos são deles). Existe uma pasta chamada public/css no projeto, e lá estão os css usados.

E como testar isso tudo? Vá até o meu repositório no GitHub e faça um clone: http://github.com/paulosuzart/moyure. Supondo que você tem o Leiningen instalado, basta fazer:


lein repl
#Deve aparecer algo como:
"REPL started; server listening on localhost:54837."
user=>
# e você faz uso do moyure.core:
user=> (use 'moyure.core)
user=> (run)

Basta acessar http://localhost:8080 e pronto. Jetty rodando com sua aplicação Web em Clojure. Você pode baixar a aplicação e modificar ela para deployar no Google App Engine como exercício, ou mesmo corrigir a função db/insert que insere duas vezes um registro ao invés de atualizá-lo.

A aplicação no GitHub tem algumas coisas não comentadas no post para tentar simplificar. Pretendo explorar mais Clojure na Web e quem sabe mostrar mais detalhes e ir mais a fundo em cada um destes frameworks/libs separadamente. Vamos ver o que consigo.

Conclusão

Clojure é uma linguagem que já ganhou espaço. Só que fora do Brasil. Existem empresas muito focadas nela para os mais variados tipos de problema como a Relevance. A insatisfação razoável com – a linguagem e não a plataforma java – para internet é algo que pode ser observado nos últimos anos, e boas alternativas já existem como Rails, Grails, Play, Django, etc. Mas nenhuma delas tem Clojure, uma linguagem puramente funcional que permite construir soluções elegantes e concisas. Esta virou sem dúvida primeira opção pra mim junto com Scala e Python. Espero ver um pouco de Clojure nos próximos grandes eventos no Brasil.

Pra matar a curiosidade de alguns e preguiça de outros, segue o print das telas (observe as urls no browser):

Update: Os testes sofreram uma pequena modificação para funcionar com a JVM persistente do Cake. Ao invés de identificadores hard coded, passei a usar o último valor do id do banco para a escrita dos testes (veja @id nos testes).

Written by paulosuzart

outubro 8, 2010 às 5:49 am

Publicado em clojure

Tagged with ,

Uma resposta

Subscribe to comments with RSS.

  1. Hello Paulo

    Thanks for the great post.

    I have a problem with some of your code though: could you clarify your intent with this line in the insert-meet function – it doesn’t work:
    (alter db nid assoc (assoc d :id nid))
    The alter function second param is indeed a function, here you pass it an int (nid).

    Thank you,
    Rollo

    Rollo Tomazzi

    dezembro 10, 2010 at 1:34 pm


Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: