advisory-circular/src/main/lemondronor/advisorycircular/util.cljs

134 lines
3.6 KiB
Clojure

(ns lemondronor.advisorycircular.util
(:require [cemerick.url :as c-url]
["fs" :as fs]
[goog.string :as gstring]
[kitchen-async.promise :as p]
[lemondronor.advisorycircular.logging :as logging]
["request-promise-native" :as request]
["js-yaml" :as yaml]))
(declare logger log-debug log-verbose log-info log-warn log-error)
(logging/deflog "util" logger)
(def fs-promises (.-promises fs))
(defn ^boolean ends-with?
[s substr]
(gstring/endsWith s substr))
;; Reads a file, returns a promise resolving to the file contents.
(defn read-file
([path]
(read-file path {}))
([path options]
(.readFile fs-promises path (clj->js options))))
;; Writes a file, returns a promise that resolves to no arguments on
;; success.
(defn write-file [path data options]
(.writeFile fs-promises path data (clj->js options)))
;; Reads a YAML config file. Returns a promise that resolves to the
;; parsed YAML.
(defn read-config [path]
(log-verbose "Reading config file %s" path)
(p/try
(p/let [data (read-file path {:encoding "utf-8"})]
(let [config (-> (yaml/safeLoad data)
(js->clj :keywordize-keys true))]
(or config {})))
(p/catch :default e
(throw (str "Error reading config file '" path "': " e)))))
;; Fetches a URL. Returns a promise that resolves to the body of the
;; response.
(defn http-get
([url]
(http-get url {}))
([url options]
(let [query (or (:query options) {})
options (dissoc options :query)
parsed-url (-> (c-url/url (str url))
(assoc :query query))
;; Work around c-url bug to re-append any stripped trailing slash.
;; https://github.com/cemerick/url/issues/24
url (if (ends-with? (str url) "/")
(str parsed-url "/")
parsed-url)]
(p/do
(log-verbose "Fetching %s" url)
(p/let [result (.get request
(clj->js (merge {:url (str url) :gzip true} options)))]
result)))))
;; From https://github.com/puppetlabs/clj-kitchensink/blob/cfea4a16e4d2e15a2d391131a163b4eeb60d872e/src/puppetlabs/kitchensink/core.clj#L311-L332
(defn deep-merge
"Deeply merges maps so that nested maps are combined rather than replaced.
For example:
(deep-merge {:foo {:bar :baz}} {:foo {:fuzz :buzz}})
;;=> {:foo {:bar :baz, :fuzz :buzz}}
;; contrast with clojure.core/merge
(merge {:foo {:bar :baz}} {:foo {:fuzz :buzz}})
;;=> {:foo {:fuzz :quzz}} ; note how last value for :foo wins"
[& vs]
(if (every? map? vs)
(apply merge-with deep-merge vs)
(last vs)))
;; From https://stackoverflow.com/a/21769626/122762
(defn nested-keys [m]
(if (map? m)
(vec
(mapcat (fn [[k v]]
(let [sub (nested-keys v)
nested (map #(into [k] %) (filter (comp not empty?) sub))]
(if (seq nested)
nested
[[k]])))
m))
[]))
(defn format-utc-ts [millis]
(let [date (js/Date. millis)]
(str (.getUTCFullYear date)
(.getUTCMonth date)
(.getUTCDate date)
"-"
(.getUTCHours date)
(.getUTCMinutes date)
(.getUTCSeconds date)
(.getUTCMilliseconds date))))
(defn feet-to-meters [f]
(* f 0.3048))
(defn timeout
([ms]
(timeout ms nil))
([ms v]
(p/promise [resolve]
(js/setTimeout #(resolve v) ms))))
(defn write-stream-to-file [stream path]
(p/promise
[resolve reject]
(let [out (fs/createWriteStream path)]
(.pipe stream out)
(.on out "finish" #(resolve nil))
(.on out "error" #(reject %)))))