понедельник, 6 ноября 2017 г.

clojure monorepo how-to

Monorepo is the way to manage several related projects from one point. In this note I'll show you how to organize monorepo using Amperity plugin lein-monolith .

0. Lets create root project and some subprojects
  rootproject
                    project.clj        ;;project file for root project
                    /apps
                            /app01     ;; project folder for app01 project
                    /libs
                            /lib01       ;; project folder for lib01 project which is needed for app01
                            /lib02       ;; some other lib


1. We should put section :plugins [[lein-monolith "1.0.1"]] to root project.clj and to project.clj of all subprojects.

(defproject rootproject/all "MONOLITH"
  :description "Mono repo"

  :plugins [[lein-monolith "1.0.1"]
                 [lein-cprint "1.3.0"]]

;; some repo which is used in all subprojects
  :repositories {"readytalk" {:url "https://dl.bintray.com/readytalk/maven/"}}

 ;; this is local deps that belongs only root project until section :dependencies is included in :inherit section. But for :inherit section we will use :managed-dependencies instead.
  :dependencies [[org.clojure/clojure "1.9.0-beta4"]]

;; this managed-deps will be delivered to all subprojects
  :managed-dependencies [[org.clojure/clojure "1.9.0-beta4"]]
 
  :monolith {
             ;; here we tell lein-monolith to merge :repositories and :managed-dependencies
             ;; to all subprojects.
             :inherit [:repositories :managed-dependencies]

            ;; here we tell that section profiles from root projects should be merged to all subprojects.
            ;; in this example we tell that in uberjar we should omit source files.
             :inherit-leaky [:profiles]

             ;; where is our subprojects located
             :project-dirs ["apps/*" "libs/*"]}

  :profiles {:uberjar {:omit-source true}}

  :env {:foo "bar"})

2. Every project.clj of all subprojects we prepare like this:

(defproject rootproject/app01 "2.0.0-SNAPSHOT"
  :description "FIXME: write description"

;; here we include lein-monolith for subproject
  :plugins [[lein-monolith "1.0.1"]
                 [lein-cprint "1.3.0"]]

 ;; here we tell that we need to inherit all sections from root project
  :monolith/inherit true
  :deployable true

  :dependencies [[rootproject/lib01 "0.1.0-SNAPSHOT"]]
  :main ^:skip-aot lib01.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

3. Now we can run commands from root project
lein monolith each do clean, uberjar 
This command will clean and make uberjars for all subprojects. Remember, that we should run
lein install
for lib01 to install to a local repo for app01 which use it. Also we can run command only for particular subproject
lein monolith each :start rootproject/app01 do uberjar 
 This command will run uberjar target only for particular project.

Interesting fact that we can point out clojure version only in root project - project.clj, in section :managed-dependencies and this version of clojure will be delivered to all subprojects. So we don't need to include clojure version in subprojects.

Also from root project we can run different tests for different subproject using selectors. First of all wee should put metadata to all deftest like here
https://github.com/technomancy/leiningen/blob/2.4.3/src/leiningen/test.clj#L164

In example above it is integrations test with ^:integration metadata. Then we should put section
:test-selectors
{:unit (complement :integration)
:integration :integration}

in root project.clj and put :test-selectors to :inherit section.  

воскресенье, 15 октября 2017 г.

clojure debugging 2

(defmacro safe-eval
  "This macro is used to execute any function inside try-catch block.
  returns value or nil in case of Exception and prints debug info. "
  [& forms]
  `(try
     ~@forms
     (catch Exception e#
       (println "--------------")
       (println "file :" *file* \newline)
       (println "s-exp:" (quote ~@forms))
       (println "src  :" ~(meta &form))
       (println "=>" (.getMessage e#)))))

воскресенье, 8 октября 2017 г.

clojure debugging

Add to project.clj
;;scope capture for debugging
 [vvvvalvalval/scope-capture "0.1.0"]

(require 'sc.api)

(defn abc [x]
  (let [y (* 2 x)
        z (Math/pow x 4)]
    (sc.api/spy {:abc [x y z]})))

(abc 2)
;;prints during execution: SPY [1 -2]  where 1 is id for defsc

(sc.api/defsc 1)

;;show me the state of local vars from let using given id 4
(sc.api/letsc 4 [x y z])
;; recreate the last operation result from let using given id 2
(sc.api/letsc 2 {:abc [x y z]})

;;show me the info map at the spy execution point using id 2
(sc.api/ep-info 2)

see video: https://vimeo.com/237220354

воскресенье, 1 октября 2017 г.

network async communication

Nikita made a good library [net.async/async "0.1.0"] for asynchronous network communication.
https://github.com/tonsky/net.async

Just add this to your project.clj

      [org.clojure/clojure "1.8.0"]
      ;; async network communications
      [net.async/async "0.1.0" :exclusions [[org.clojure/clojure]
                                            [org.clojure/tools.logging]
                                            [org.clojure/core.async]]]

      [org.clojure/core.async "0.3.443" :exclusions [org.clojure/tools.reader]]

      [org.clojure/tools.logging "0.4.0"]

Here is an output of example below:
-------------------------------
server:  4263
client:  ECHO/4263
-------------------------------
server:  16640
client:  ECHO/16640
-------------------------------
server:  75968
client:  ECHO/75968
-------------------------------
server:  11092
client:  ECHO/11092
-------------------------------
server:  61707
client:  ECHO/61707
-------------------------------
server:  50773
client:  ECHO/50773
-------------------------------
server:  16803
client:  ECHO/16803
-------------------------------

Here is a source code of echo server and client:

(require '[clojure.core.async :refer [<! >!  <!! >!! close! go]])
(use 'net.async.tcp)

(def event-l-client (event-loop))
(def event-l-server (event-loop))

(defn echo-server [evt-loop]
  (let [acceptor (accept evt-loop {:port 8899})]
    (loop []
      (when-let [server (<!! (:accept-chan acceptor))]
        (go
          (loop []
            (when-let [msg (<! (:read-chan server))]
              (when-not (keyword? msg)
                (>! (:write-chan server) (.getBytes (str "ECHO/" (String. msg))))
                (println "server: " (String. msg)))
              (recur))))
        (recur)))))


(defn echo-client [evt-loop]
  (let [client (connect evt-loop {:host "127.0.0.1" :port 8899})]
    (loop []
      (go (>! (:write-chan client) (.getBytes (str (rand-int 100000)))))
      (loop []
        (let [read (<!! (:read-chan client))]
          (when (bytes? read) (println "client: " (String. read)))
          (when (and (keyword? read)
                     (not= :connected read))
            (println "disconnected from server. trying to reconnect using timeout")
            (recur))))
      (println "-------------------------------")
      (Thread/sleep (rand-int 3000))
      (recur))))


(future (echo-server event-l-server))

(future (echo-client event-l-client))

(shutdown! event-l-server)
(shutdown! event-l-client)

среда, 23 августа 2017 г.

react learning: sorting table

(enable-console-print!)
(println "js app is started.")

(def excel-data (reagent/atom {:h ["book" "author" "language" "published" "sales"]
                               :b [["The Lord of the Rings" "J. R. R. Tolkien" "English" "1954–1955" "150 million"]
                                   ["Le Petit Prince (The Little Prince)" "Antoine de Saint-Exupéry" "French" "1943" "140 million"]
                                   ["Harry Potter and the Philosopher's Stone" "J. K. Rowling" "English" "1997" "107 million"]
                                   ["And Then There Were None" "Agatha Christie" "English" "1939" "100 million"]
                                   ["Dream of the Red Chamber" "Cao Xueqin" "Chinese" "1754–1791" "100 million"]
                                   ["The Hobbit" "J. R. R. Tolkien" "English" "1937" "100 million"]
                                   ["She: A History of Adventure" "H. Rider Haggard" "English" "1887" "100 million"]]
                               :sort-type {true > false <}
                               :sort-current true
                               :sort-column 0}))

(defn table-body
  [body]
  (for [row body]
    ^{:key (gensym)} [:tr (for [cell row]
                            ^{:key (gensym)}  [:td cell])]))

(defn change-sort-direction-if-needed
  [column-index]
  ;; when click on the same column then change sort direction
  (when (= column-index (:sort-column @excel-data))
    (swap! excel-data assoc-in [:sort-current] (not (:sort-current @excel-data)))))

(defn sort-table [column-index]
  (change-sort-direction-if-needed column-index)
  (let [sort-fn ((:sort-type @excel-data) (:sort-current @excel-data))
        sorted-data (sort-by #(nth % column-index) sort-fn (:b @excel-data))]
    (swap! excel-data assoc-in [:b] sorted-data)
    (swap! excel-data assoc-in [:sort-column] column-index)))

(defn table-header
  [header]
  [:tr
   (doall
    (for [header-item header]
      ^{:key (gensym)} [:th (if (= (.indexOf header header-item) (:sort-column @excel-data))
                              (str header-item (if (:sort-current @excel-data) \u2193 \u2191))
                              header-item)]))])

(defn excel-app
  "Excel prototype application"
  [excel-data]
  [:div
   [:table
    [:thead {:on-click #(sort-table (-> % .-target .-cellIndex))} (table-header (:h @excel-data))]
    [:tbody (table-body (:b @excel-data))]]])

(reagent/render-component [excel-app excel-data]
                          (.getElementById js/document "app"))



looks like this:



вторник, 22 августа 2017 г.

react learning: simple table

(enable-console-print!)
(println "js app is started.")

(def excel-data (reagent/atom {:h ["book" "author" "language" "published" "sales"]
                               :b [["The Lord of the Rings" "J. R. R. Tolkien" "English" "1954–1955" "150 million"]
                                   ["Le Petit Prince (The Little Prince)" "Antoine de Saint-Exupéry" "French" "1943" "140 million"]
                                   ["Harry Potter and the Philosopher's Stone" "J. K. Rowling" "English" "1997" "107 million"]
                                   ["And Then There Were None" "Agatha Christie" "English" "1939" "100 million"]
                                   ["Dream of the Red Chamber" "Cao Xueqin" "Chinese" "1754–1791" "100 million"]
                                   ["The Hobbit" "J. R. R. Tolkien" "English" "1937" "100 million"]
                                   ["She: A History of Adventure" "H. Rider Haggard" "English" "1887" "100 million"]]}))

(defn table-body
  [body]
  (for [row body]
    [:tr (for [cell row]
           [:td cell])]))

(defn table-header
  [header]
  [:tr (for [e header]
         [:th e])])

(defn excel-app
  "Excel prototype application"
  [excel-data]
  [:div
   [:table
    [:thead (table-header (:h @excel-data))]
    [:tbody (table-body (:b @excel-data))]]])

(reagent/render-component [excel-app excel-data]
                          (.getElementById js/document "app"))



It looks like this:



PS: Use ^{:key (gensym)} before :th, :tr, :td to avoid warning about unique key

воскресенье, 20 августа 2017 г.

react learning: shared state example

Using Reagent

(def str-area (reagent/atom "init"))
(def length (reagent/atom 0))

(defn my-text-area [area-value]
  [:textarea {:id :comments
              :defaultValue @area-value
              :on-change #(do
                            (reset! str-area (-> % .-target .-value))
                            (reset! length (count (-> % .-target .-value))))}])

(defn text-counter []
  [:div
   "Counter: " @length])

(defn text-value []
  [:div
   "Value: " @str-area])

(defn app []
  [:div
   [my-text-area str-area]
   [text-counter]
   [text-value]])

(reagent/render-component [app]
                          (.getElementById js/document "app"))

It looks like this:


Using RUM

(enable-console-print!)

(rum/defc count-label
  [value]
  [:div [:label (str "Count:" (count @value))]])

(rum/defcs app < (rum/local "hi!" ::value)
  [state]
  (let [v (::value state)]
    [:div
     [:div [:textarea {:default-value @v :on-change (fn [e] (reset! v (-> e .-target .-value)))}]]
     (count-label v)
     [:div [:label (str "Value:" @v)]]]))

(rum/mount (app) (.getElementById js/document "app"))