How I wrote a web application using only clojure



    Recently I met an interesting language - clojure. I immediately liked the lazy and immutable collections, stm, macros, lots of brackets and dsl for all occasions.
    And I decided to try to make a web application using only clojure.

    application


    It was intended to create a simple subtitle seeker that:
    • every 5 minutes indexes new subtitles on addicted, notabenoid and other services;
    • has a one-page web-interface with search without page reloading;
    • shows in the web-interface the number of indexed subtitles and changes it when new ones appear;
    • has a simple api for interacting with the desktop client .

    Parsers


    Surprisingly, parsers were simple and convenient to write. At first, it seemed that there were a lot of brackets, but threading macros ( -> , - >> , ->, and - <>> - passing the result of the argument to the next expression) helped a lot.

    For example, a piece of the notabenoid parser doing the same thing in python and clojure:
    clojurepython
    (defn get-release-page-result
      "Get release page result"
      [page]
      (-<>> (get-release-page-url page)
            helpers/fetch
            (html/select <> [:ul.search-results :li :p :a])
            (map (helpers/make-safe book-from-line nil))
            (remove nil?)
            (map episodes-from-book)
            flatten))
    
    def get_release_page_result(page):
        """Get release page result"""
        url = get_release_page_url(page)
        content = requests.get(url).content
        soup = BeautifulSoup(content)
        for line in get_lines_from_soup(soup):
            book = get_book_from_line(line)
            if book:
                yield from get_episodes_from_book(book)
    
    16 brackets14 brackets
    The at-at library is used to start parsers, and enlive is used for parsing html . The result is written to elasticsearch.

    Server side


    Server

    As the server, I chose the http-kit , mainly because I wanted web sockets. And it’s very simple to use them here, for example, sending all the clients the number of indexed subtitles after the update will look like this:
    (add-watch total-count :notifications
               #(doseq [con @subscribers]
                  (send! con (prn-str {:total-count %4}))))
    

    Routing

    For routing - compojure . There are no differences from django and other popular frameworks:
    (defroutes main-routes
      (GET "/" [] (views/index-page))
      (GET "/api/list-languages/" {params :params} (api/list-languages params))
      (GET "/notifications/" [] push/notifications)
      (route/resources const/static-path))
    

    API

    Since we use clojure everywhere, our api should return the result in the native data structures and in json (for the desktop client in python). I couldn’t find the library that could (I already found) , so I had to cycle a bit and invent my own mini dsl:
    (defn- get-writer
      "Get writer from params"
      [params]
      (if (= (:format params) "json")
        json/write-str
        prn-str))
    (defmacro defapi
      "Define api method"
      [name doc args & body]
      `(defn ~name ~args
         ((get-writer (first ~args))
          ~@body)))
    

    And as a simple use case:
    (defapi list-languages
      "List all available languages"
      [params]
      (models/list-languages))
    

    View

    For html rendering, I used a special dsl - hiccup , the template with it looks a bit "Martian":
    (defn index-page []
      (html5 [:head
              [:title "Subman - subtitle search service"]
             [:body
              [:h1 "Welcome to subman!"]]))
    

    Styles

    For styles, clojure also has its own dsl - garden . The code with it also looks strange:
    (defstyles main
      [:.search-input {:z-index 100
                       :background-color "#fff"}]
      [:.info-box {:text-align "center"
                   :font-size (px 18)}]
      [:.search-result-holder {:padding-left 0
                               :padding-right 0}])
    

    Client part


    I wrote the client part not entirely in clojure, but in clojurescript, which eventually compiles into javascript. As a framework, I used reagent - binding for react.js for clojure, not checking objects for changes every second (thanks to atoms) and using hiccup-like dsl to describe components:
    (defn info-box
      "Show info box"
      [text]
      [:div.container.col-xs-12.info-box
       [:h2 text]])
    

    Everything is very good here, until you need to work directly with js libraries. For example, the code for connecting typeahead to the search field:
    (defn init-autocomplete
      "Initiale autocomplete"
      [query langs sources]
      (let [input ($ "#search-input")]
        (.typeahead input
                    (js-obj "highlight" true)
                    (js-obj "source"
                            (fn [query cb]
                              (cb (apply array
                                         (take const/autocomplete-limit
                                               (map #(js-obj "value" %)
                                                    (get-completion query
                                                                    @langs
                                                                    @sources))))))))
        (.on input "typeahead:closed" (fn []
                                        (reset! query (.val input))))))
    

    UPD: After a little refactoring, the code became less scary:
    (defn completion-source
      "Source for typeahead autocompletion"
      [langs sources query cb]
      (cb (->> (get-completion query
                               @langs
                               @sources)
               (map #(js-obj "value" %))
               (take const/autocomplete-limit)
               (apply array))))
    (defn init-autocomplete
      "Initiale autocomplete"
      [query langs sources]
      (let [input ($ "#search-input")]
        (.typeahead input
                    #js {:highlight true}
                    #js {:source (partial completion-source
                                          langs sources)})
        (.on input "typeahead:closed"
             #(reset! query (.val input)))))
    

    And even the size of the “compiled” file was not so big - only 290kb.

    As a huge plus, using clojure along with clojurescript - you can write one code for the client and server using cljx .

    conclusions


    Although clojure allows you to develop web applications without the knowledge and use of html, css and javascript, I would not dare to do production projects like this.

    The source code of the result.
    The result itself.

    Also popular now: