axiom//leiningen.axiom Leiningen tasks for Axiom

Author: Temporarily Removed  (temporarily@removed.com)
Date: 25 April 2018
Repository: https://github.com/temporarily/removed
Version: 0.4.1

1    Introduction

lein-axiom is a leiningen plugin for automating Axiom-related tasks. It provides the sub-tasks deploy and run.

(-> #'axiom meta :doc) => "Automating common Axiom tasks"
 (-> #'axiom meta :subtasks) => [#'deploy #'run #'deps #'pprint #'inspect]
 (-> #'deploy meta :doc) => "Deploy the contents of the project to the configured Axiom instance"
 (-> #'run meta :doc) => "Run an Axiom instance"
 (-> #'deps meta :doc) => "Recursively add permacode dependencies to this project"
 (-> #'pprint meta :doc) => "Prints the contents of the given permacode module"
 (-> #'inspect meta :doc) => "Performs a database query based on the given partial event"

The lein-axiom plugin requires the keys :axiom-deploy-config and :axiom-run-config to be present and contain configuration maps. These configuration maps are used to initialize DI injectors so that Axiom's components are available to the plugin. :axiom-run-config is used by lein axiom run, while :axiom-deploy-config is used by all other tasks.

2    deploy

lein axiom deploy stores the contents of the project using a hasher, publishes an axiom/perm-versions event. This event is then handled by Axiom's migrator to deploy the code, and by Axiom's gateway to serve static files.

(let [published (atom [])
     project {:axiom-deploy-config
              {:publish (partial swap! published conj)
               :deploy-dir (fn [ver dir publish]
                             (publish {:ver ver
                                       :dir dir}))
               :uuid (constantly "ABCDEFG")}}]
 (axiom project "deploy") => nil
 (provided
  (rand-int 10000000) => 5555)
 @published => [{:ver "dev-5555"
                 :dir "."}])

3    run

lein axiom run starts an instance of Axiom based on a merge of :axiom-deploy-config and :axiom-run-config, with pereference to the latter. It will remain running until interrupted (Ctrl+C) by the user.

(let [project {:axiom-run-config {:http-config {:port 33333}}
              :axiom-deploy-config
              {:ring-handler (fn [req] {:status 200
                                        :body "Hello"})
               :http-config {:port 44444} ;; This will be overridden by :axiom-run-config
               }}
     fut (future
           (axiom project "run"))]
 (Thread/sleep 100)
 (let [res @(http/get "http://localhost:33333")]
   (:status res) => 200
   (-> res :body slurp) => "Hello"))

4    deps

.clj files in Axiom apps are requied to be valid permacode source files. This means, among other things, that they cannot :require any namespaces, except for a small white-list of pure ones. The way to use external libraries (such as cloudlog.core, which defines the defrule and defclause macros), one needs to :require them in the form perm.*, where * is replaced with a hash-code representing the dependency's content. Once deployed on Axiom, Axiom will look up these dependencies by their hash-code. However, for the purpose of running this code in the development environment we need a way to also add the dependencies to the project.

Running lein axiom deps will look-up all .clj files in the project's :source-paths, and for each such file will extract the dependent perm.* namespaces. Then, it will use the hasher configured in :axiom-deploy-config to extract these dependencies and write them to .clj files in the project's first source-path, if they don't already exist.

(let [project {:axiom-deploy-config {:hasher [..hash.. ..unhash..]}
              :source-paths ["/path/to/proj/src1" "/path/to/proj/src2"]}]
 (axiom project "deps") => nil
 (provided
  (all-source-files project) => [..src1.. ..src2..]
  (required-perms ..src1..) => ["FOO" "BAR"]
  (required-perms ..src2..) => ["BAZ"]
  (io/file "/path/to/proj/src1") => ..src-dir..
  (create-perm-file "FOO" [..hash.. ..unhash..] ..src-dir..) => false
  (create-perm-file "BAR" [..hash.. ..unhash..] ..src-dir..) => false
  (create-perm-file "BAZ" [..hash.. ..unhash..] ..src-dir..) => false))

If new perm.* namespaces are introduced, lein axiom deps works iteratively to fetch their dependencies.

(let [project {:axiom-deploy-config {:hasher [..hash.. ..unhash..]}
              :source-paths ["/path/to/proj/src1"]}]
 (axiom project "deps") => nil
 (provided
  (all-source-files project) => [..src1.. ..src2..]
  (required-perms ..src1..) =streams=> [["FOO" "BAR"]
                                        ["FOO" "BAR" "QUUX"]]
  (required-perms ..src2..) => ["BAZ"]
  (io/file "/path/to/proj/src1") => ..src-dir..
  (create-perm-file "FOO" [..hash.. ..unhash..] ..src-dir..) => false
  (create-perm-file "BAR" [..hash.. ..unhash..] ..src-dir..) =streams=> [true false]
  (create-perm-file "BAZ" [..hash.. ..unhash..] ..src-dir..) => false
  (create-perm-file "QUUX" [..hash.. ..unhash..] ..src-dir..) => false))

5    pprint

lein axiom pprint pretty-prints the content of a permacode module, associated with the given hashcode. It uses the hasher specified in the project's :axiom-deploy-config to fetch the module.

(let [project {:axiom-deploy-config {:hasher [(fn [])
                                             (fn [hashcode]
                                               (when-not (= hashcode "THEHASHCODE")
                                                 (throw (Exception. "Bad hashcode")))
                                               {:some :content})]}}]
 (axiom project "pprint" "THEHASHCODE") => nil
 (provided
  (ppr/pprint {:some :content}) => irrelevant))

6    inspect

lein axiom inspect prints the events stored in the database, matching a partial event. It takes one parameter which is a map represented in EDN format. This map should include the fields :kind (:fact or :rule), :name and :key. It prints out the matching events in EDN format.

(let [db-chan (async/chan 10)
     project {:axiom-deploy-config {:database-chan db-chan}}
     future (async/thread
              (axiom project "inspect" "{:kind :fact :name \"foo/bar\" :key 123}"))]
 (let [[[q repl-ch] ch] (async/alts!! [db-chan (async/timeout 1000)])
       ev1 {:kind :fact
            :name "foo/bar"
            :key 123
            :data [1 2 3]}
       ev2 {:kind :fact
            :name "foo/bar"
            :key 123
            :data [2 3 4]}]
   ch => db-chan
   q => {:kind :fact :name "foo/bar" :key 123}
   (async/>!! repl-ch ev1)
   (async/>!! repl-ch ev2)
   (async/close! repl-ch)
   (async/<!! future) => nil
   ;; @output => (str "\n" (pr-str ev1) "\n" (pr-str ev2))
   ))

7    Under the Hood

7.1    all-source-files

Given a project, all-source-files returns all the *.clj files located under all :source-paths in the project.

(all-source-files {:source-paths ["/path/to/proj/src1"
                                 "/path/to/proj/src2"]}) => [(io/file "/path/to/proj/src1/foo.clj")
                                                             (io/file "/path/to/proj/src2/bar.clj")
                                                             (io/file "/path/to/proj/src2/baz.clj")]
 (provided
(file-seq (io/file "/path/to/proj/src1")) => [(io/file "/path/to/proj/src1/foo.clj")
                                              (io/file "/path/to/proj/src1/x.cljs")
                                              (io/file "/path/to/proj/src1/index.html")]
(file-seq (io/file "/path/to/proj/src2")) => [(io/file "/path/to/proj/src2/bar.clj")
                                              (io/file "/path/to/proj/src2/baz.clj")])

7.2    required-perms

Given a file, required-perms returns all the permacode hash-codes required by that source file.

(required-perms ..file..) => ["FOO" "BAR" "BAZ"]
 (provided
(publish/get-ns ..file..) => '(ns some.ns
                                (:require [clojure.string :as str]
                                          [perm.FOO :as foo])
                                (:require [perm.BAR]
                                          [perm.BAZ :as baz])))

7.3    create-perm-file

Given a hash-code, a hasher and a source path, create-perm-file creates a .clj source file with the underlying content. The file will be placed under the perm directory because the namespace is named perm.* (where * is the given hash-code). Its ns header will be updated to include the perm.* name. It returns whether a new file needed to be created.

(let [hasher [(fn [code content]
               (throw (Exception. "This should not be called")))
             (fn [hashcode]
               '[(ns original.name
                   (:require [something]))
                 (foo 123)
                 (bar 234)])]
     source-dir (io/file "/tmp" (str "testsrc-" (rand-int 100000)))]
 (create-perm-file "FOO" hasher source-dir) => true
 (-> (io/file source-dir "perm") .exists) => true
 (-> (io/file source-dir "perm" "FOO.clj") slurp) => "(ns perm.FOO (:require [something]))(foo 123)(bar 234)"
 (create-perm-file "FOO" hasher source-dir) => false ;; This file already exists
 )