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.
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 "."}])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")).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))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))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))
))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")])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])))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
)