Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API for use as -X program or -T CLI tool #10

Merged
merged 22 commits into from
Mar 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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