Skip to content

Commit

Permalink
Merge pull request #10 from athos/feature/api
Browse files Browse the repository at this point in the history
Add API for use as `-X` program or `-T` CLI tool
  • Loading branch information
athos authored Mar 16, 2022
2 parents 1920164 + 685159a commit 3125449
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 0 deletions.
1 change: 1 addition & 0 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{:paths ["src"]
:deps {org.clojure/clojure {:mvn/version "1.10.3"}}
:tools/usage {:ns-default pogonos.api}
:aliases {:check
{:extra-deps
{athos/clj-check {:git/url "https://github.com/athos/clj-check.git"
Expand Down
143 changes: 143 additions & 0 deletions src/pogonos/api.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
(ns pogonos.api
(:require [clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.string :as str]
[pogonos.core :as pg]
[pogonos.error :as error]
[pogonos.output :as out]
[pogonos.reader :as reader])
(:import [java.io File PushbackReader]
[java.util.regex Pattern]))

(defn render
"Renders the given Mustache template.
One of the following option can be specified as a template source:
- :string Renders the given template string
- :file Renders the specified template file
- :resource Renders the specified template resource on the classpath
If none of these are specified, the template will be read from stdin.
The following options can also be specified:
- :output Path to the output file. If not specified, the rendering result
will be emitted to stdout by default.
- :data Map of the values passed to the template
- :data-file If specified, reads an edn map from the file specified by that
path and pass it to the template"
{:added "0.2.0"}
[{:keys [string file resource data data-file output] :as opts}]
(let [data (or (when data-file
(with-open [r (-> (io/reader (str data-file))
PushbackReader.)]
(edn/read r)))
data)
out (if output
(out/to-file (str output))
(out/to-stdout))
opts' (-> opts
(assoc :output out)
(dissoc :string :file :resource :data :data-file))]
(cond string (pg/render-string string data opts')
file (pg/render-file (str file) data opts')
resource (pg/render-resource (str resource) data opts')
:else (pg/render-input (reader/->reader *in*) data opts'))))

(def ^:private ^:const path-separator
(re-pattern (Pattern/quote (System/getProperty "path.separator"))))

(defn- ->matcher [x]
(let [regexes (if (coll? x)
(mapv (comp re-pattern str) x)
[(re-pattern (str x))])]
(fn [s]
(boolean (some #(re-find % s) regexes)))))

(def ^:private ^:dynamic *errors*)

(defn- with-error-handling [opts f]
(try
(f)
(catch Exception e
(if (::error/type (ex-data e))
(do (when (or (not (:quiet opts)) (:only-show-errors opts))
(binding [*out* *err*]
(println "[ERROR]" (ex-message e))))
(set! *errors* (conj *errors* e)))
(throw e)))))

(defn- check-inputs [inputs {:keys [include-regex exclude-regex] :as opts}]
(let [include? (if include-regex
(->matcher include-regex)
(constantly true))
exclude? (if exclude-regex
(->matcher exclude-regex)
(constantly false))]
(doseq [{:keys [name input]} inputs
:when (and (include? name) (not (exclude? name)))]
(when (and (not (:quiet opts)) (not (:only-show-errors opts)))
(binding [*out* *err*]
(println "Checking template" name)))
(with-open [r (reader/->reader input)]
(with-error-handling opts
#(pg/check-input r (assoc opts :source name)))))))

(defn- check-files [files opts]
(-> (for [file files
:let [file (io/file file)]]
{:name (.getPath file) :input file})
(check-inputs opts)))

(defn- check-resources [resources opts]
(-> (for [res resources]
{:name res :input (io/resource res)})
(check-inputs opts)))

(defn- check-dirs [dirs opts]
(-> (for [dir dirs
^File file (file-seq (io/file dir))
:when (.isFile file)]
file)
(check-files opts)))

(defn- split-path [path]
(if (sequential? path)
(mapv str path)
(str/split (str path) path-separator)))

(defn check
"Checks if the given Mustache template contains any syntax error.
The following options cab be specified as a template source:
- :string Checks the given template string
- :file Checks the specified template file
- :dir Checks the template files in the specified directory
- :resource Checks the specified template resource on the classpath
If none of these are specified, the template will be read from stdin.
For the :file/:dir/:resource options, two or more files/directories/resources
may be specified by delimiting them with the file path separator (i.e. ':' (colon)
on Linux/macOS and ';' (semicolon) on Windows).
When multiple templates are checked using the :file/:dir/:resource options,
they can be filtered with the :include-regex and/or exclude-regex options.
The verbosity of the syntax check results may be adjusted to some extent with
the following options:
- :only-show-errors Hides progress messages
- :suppress-verbose-errors Suppresses verbose error messages"
{:added "0.2.0"}
[{:keys [string file dir resource on-failure] :or {on-failure :exit} :as opts}]
(binding [*errors* []]
(cond string (with-error-handling opts #(pg/check-string string opts))
file (check-files (split-path file) opts)
resource (check-resources (split-path resource) opts)
dir (check-dirs (split-path dir) opts)
:else (with-error-handling opts
#(pg/check-input (reader/->reader *in*) opts)))
(when (seq *errors*)
(case on-failure
:exit (System/exit 1)
:throw (throw (ex-info "Template checking failed" {:errors *errors*}))
nil))))
1 change: 1 addition & 0 deletions test-resources/data.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{:name "Clojurian"}
1 change: 1 addition & 0 deletions test-resources/templates/hello.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello, {{name}}!
122 changes: 122 additions & 0 deletions test/pogonos/api_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
(ns pogonos.api-test
(:require [clojure.java.io :as io]
[clojure.test :refer [deftest is are testing]]
[pogonos.api :as api]
[clojure.string :as str]))

(defn test-file [path]
(str (io/file (io/resource path))))

(deftest render-test
(are [opts expected] (= expected (with-out-str (api/render opts)))
{:string "Hello, {{name}}!" :data {:name "Clojurian"}}
"Hello, Clojurian!"

{:file (test-file "templates/hello.mustache")
:data-file (test-file "data.edn")}
"Hello, Clojurian!\n"

{:resource "templates/hello.mustache" :data {:name "Clojurian"}}
"Hello, Clojurian!\n")
(is (= "Hello, Clojurian!"
(with-out-str
(with-in-str "Hello, {{name}}!"
(api/render {:data {:name "Clojurian"}}))))))

(defn- with-stderr-lines [f]
(-> (with-out-str
(binding [*err* *out*]
(f)))
(str/split #"\n")))

(defn- join-paths [& paths]
(str/join (System/getProperty "path.separator") paths))

(deftest check-test
(testing "basic use cases"
(is (nil? (api/check {:string "{{foo}}" :quiet true})))
(is (thrown? Exception
(api/check {:string "{{foo" :quiet true :on-failure :throw})))
(is (nil? (api/check {:file (test-file "templates/hello.mustache")
:quiet true})))
(is (thrown? Exception
(api/check {:file (test-file "templates/broken.mustache")
:quiet true :on-failure :throw})))
(is (nil? (api/check {:resource "templates/hello.mustache" :quiet true})))
(is (thrown? Exception
(api/check {:resource "templates/broken.mustache"
:quiet true :on-failure :throw})))
(is (nil? (with-in-str "{{foo}}" (api/check {:quiet true}))))
(is (thrown? Exception
(with-in-str "{{foo"
(api/check {:quiet true :on-failure :throw})))))
(testing "bulk check"
(testing "files"
(is (= [(str "Checking template " (test-file "templates/hello.mustache"))
(str "Checking template " (test-file "templates/main.mustache"))]
(with-stderr-lines
#(api/check {:file [(test-file "templates/hello.mustache")
(test-file "templates/main.mustache")]}))))
(let [lines (with-stderr-lines
#(api/check {:file (join-paths
(test-file "templates/hello.mustache")
(test-file "templates/broken.mustache"))
:on-failure nil
:suppress-verbose-errors true}))]
(is (= (count lines) 3))
(is (= (str "Checking template " (test-file "templates/hello.mustache"))
(nth lines 0)))
(is (= (str "Checking template " (test-file "templates/broken.mustache"))
(nth lines 1)))
(is (str/starts-with? (nth lines 2) "[ERROR]"))))
(testing "resources"
(is (= ["Checking template templates/main.mustache"
"Checking template templates/node.mustache"]
(with-stderr-lines
#(api/check {:resource ["templates/main.mustache"
"templates/node.mustache"]}))))
(let [lines (with-stderr-lines
#(api/check {:resource (join-paths "templates/broken.mustache"
"templates/hello.mustache")
:on-failure nil
:suppress-verbose-errors true}))]
(is (= (count lines) 3))
(is (= "Checking template templates/broken.mustache" (nth lines 0)))
(is (str/starts-with? (nth lines 1) "[ERROR]"))
(is (= "Checking template templates/hello.mustache" (nth lines 2)))))
(testing "dirs"
(let [lines (sort
(with-stderr-lines
#(api/check {:dir (test-file "templates")
:on-failure nil
:suppress-verbose-errors true})))]
(is (= [(str "Checking template " (test-file "templates/broken.mustache"))
(str "Checking template " (test-file "templates/demo.mustache"))
(str "Checking template " (test-file "templates/hello.mustache"))
(str "Checking template " (test-file "templates/main.mustache"))
(str "Checking template " (test-file "templates/node.mustache"))]
(butlast lines)))
(is (str/starts-with? (last lines) "[ERROR]")))
(let [lines (with-stderr-lines
#(api/check {:dir (test-file "templates")
:only-show-errors true
:on-failure nil
:suppress-verbose-errors true}))]
(is (= (count lines) 1))
(is (str/starts-with? (first lines) "[ERROR]")))
(let [lines (sort
(with-stderr-lines
#(api/check {:dir [(test-file "templates")]
:include-regex ["broken.mustache$" "hello.mustache$"]
:on-failure nil
:suppress-verbose-errors true})))]
(is (= [(str "Checking template " (test-file "templates/broken.mustache"))
(str "Checking template " (test-file "templates/hello.mustache"))]
(butlast lines)))
(is (str/starts-with? (last lines) "[ERROR]")))
(is (= [(str "Checking template " (test-file "templates/hello.mustache"))
(str "Checking template " (test-file "templates/main.mustache"))]
(sort
(with-stderr-lines
#(api/check {:dir (test-file "templates")
:exclude-regex "(broken|demo|node).mustache$"}))))))))
2 changes: 2 additions & 0 deletions test/pogonos/test_runner.cljc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(ns pogonos.test-runner
(:require [clojure.test :as t]
pogonos.spec-test
#?(:clj pogonos.api-test)
pogonos.core-test
pogonos.output-test
pogonos.parse-test
Expand All @@ -11,6 +12,7 @@

(defn -main []
(t/run-tests 'pogonos.spec-test
#?(:clj 'pogonos.api-test)
'pogonos.core-test
'pogonos.output-test
'pogonos.parse-test
Expand Down

0 comments on commit 3125449

Please sign in to comment.