291 lines
8.5 KiB
Clojure
291 lines
8.5 KiB
Clojure
(ns no.en.core
|
|
(:refer-clojure :exclude [replace read-string])
|
|
(:require [clojure.string :refer [blank? join replace split upper-case]]
|
|
#?(:clj [clojure.edn :refer [read-string]])
|
|
#?(:cljs [cljs.reader :refer [read-string]])
|
|
#?(:cljs [goog.crypt.base64 :as base64]))
|
|
#?(:clj (:import [java.net URLEncoder URLDecoder]
|
|
[org.apache.commons.codec.binary Base64])))
|
|
|
|
(def port-number
|
|
{:amqp 5672
|
|
:http 80
|
|
:https 443
|
|
:mysql 3306
|
|
:postgresql 5432
|
|
:rabbitmq 5672
|
|
:zookeeper 2181})
|
|
|
|
(def url-regex #"([^:]+)://(([^:]+):([^@/]+)@)?(([^:/]+)(:([0-9]+))?((/[^?#]*)(\?([^#]*))?)?)(\#(.*))?")
|
|
|
|
(defn split-by-regex
|
|
"Split the string `s` by the regex `pattern`."
|
|
[s pattern]
|
|
(if (sequential? s)
|
|
s (if-not (blank? s)
|
|
(split s pattern))))
|
|
|
|
(defn split-by-comma
|
|
"Split the string `s` by comma."
|
|
[s] (split-by-regex s #"\s*,\s*"))
|
|
|
|
(defn utf8-string
|
|
"Returns `bytes` as an UTF-8 encoded string."
|
|
[bytes]
|
|
#?(:clj (String. bytes "UTF-8")
|
|
:cljs (throw (ex-info "utf8-string not implemented yet" bytes))))
|
|
|
|
(defn base64-encode
|
|
"Returns `s` as a Base64 encoded string."
|
|
[bytes]
|
|
(when bytes
|
|
#?(:clj (String. (Base64/encodeBase64 bytes))
|
|
:cljs (base64/encodeString bytes false))))
|
|
|
|
(defn base64-decode
|
|
"Returns `s` as a Base64 decoded string."
|
|
[s]
|
|
(when s
|
|
#?(:clj (Base64/decodeBase64 (.getBytes s))
|
|
:cljs (base64/decodeString s false))))
|
|
|
|
(defn compact-map
|
|
"Removes all map entries where the value of the entry is empty."
|
|
[m]
|
|
(reduce
|
|
(fn [m k]
|
|
(let [v (get m k)]
|
|
(if (or (nil? v)
|
|
(and (or (map? v)
|
|
(sequential? v))
|
|
(empty? v)))
|
|
(dissoc m k) m)))
|
|
m (keys m)))
|
|
|
|
(defn url-encode
|
|
"Returns `s` as an URL encoded string."
|
|
[s & [encoding]]
|
|
(when s
|
|
#?(:clj (-> (URLEncoder/encode (str s) (or encoding "UTF-8"))
|
|
(replace "%7E" "~")
|
|
(replace "*" "%2A")
|
|
(replace "+" "%20"))
|
|
:cljs (-> (js/encodeURIComponent (str s))
|
|
(replace "*" "%2A")))))
|
|
|
|
(defn url-decode
|
|
"Returns `s` as an URL decoded string."
|
|
[s & [encoding]]
|
|
(when s
|
|
#?(:clj (URLDecoder/decode s (or encoding "UTF-8"))
|
|
:cljs (js/decodeURIComponent s))))
|
|
|
|
(defn pow [n x]
|
|
#?(:clj (Math/pow n x)
|
|
:cljs (.pow js/Math n x)))
|
|
|
|
(def byte-scale
|
|
{"B" (pow 1024 0)
|
|
"K" (pow 1024 1)
|
|
"M" (pow 1024 2)
|
|
"G" (pow 1024 3)
|
|
"T" (pow 1024 4)
|
|
"P" (pow 1024 5)
|
|
"E" (pow 1024 6)
|
|
"Z" (pow 1024 7)
|
|
"Y" (pow 1024 8)})
|
|
|
|
(defn- apply-unit [number unit]
|
|
(if (string? unit)
|
|
(case (upper-case unit)
|
|
(case unit
|
|
"M" (* number 1000000)
|
|
"B" (* number 1000000000)))
|
|
number))
|
|
|
|
(defn- parse-number [s parse-fn]
|
|
(if-let [matches (re-matches #"\s*([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)(M|B)?.*" (str s))]
|
|
#?(:clj
|
|
(try (let [number (parse-fn (nth matches 1))
|
|
unit (nth matches 3)]
|
|
(apply-unit number unit))
|
|
(catch NumberFormatException _ nil))
|
|
:cljs
|
|
(let [number (parse-fn (nth matches 1))
|
|
unit (nth matches 3)]
|
|
(if-not (js/isNaN number)
|
|
(apply-unit number unit))))))
|
|
|
|
(defn parse-bytes [s]
|
|
(if-let [matches (re-matches #"\s*([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)(B|K|M|G|T|P|E|Z|Y)?.*" (str s))]
|
|
(let [number (read-string (nth matches 1))
|
|
unit (nth matches 3)]
|
|
(long (* (long (read-string (str (nth matches 1))))
|
|
(get byte-scale (upper-case (or unit "")) 1))))))
|
|
|
|
(defn parse-integer
|
|
"Parse `s` as a integer number."
|
|
[s]
|
|
(parse-number s #(#?(:clj Integer/parseInt :cljs js/parseInt) %1)))
|
|
|
|
(defn parse-long
|
|
"Parse `s` as a long number."
|
|
[s]
|
|
(parse-number s #(#?(:clj Long/parseLong :cljs js/parseInt) %1)))
|
|
|
|
(defn parse-double
|
|
"Parse `s` as a double number."
|
|
[s]
|
|
(parse-number s #(#?(:clj Double/parseDouble :cljs js/parseFloat) %1)))
|
|
|
|
(defn parse-float
|
|
"Parse `s` as a float number."
|
|
[s]
|
|
(parse-number s #(#?(:clj Float/parseFloat :cljs js/parseFloat) %1)))
|
|
|
|
(defn format-query-params
|
|
"Format the map `m` into a query parameter string."
|
|
[m]
|
|
(let [params (->> (sort-by first (seq m))
|
|
(remove #(blank? (str (second %1))))
|
|
(map #(vector (url-encode (name (first %1)))
|
|
(url-encode (second %1))))
|
|
(map #(join "=" %1))
|
|
(join "&"))]
|
|
(if-not (blank? params)
|
|
params)))
|
|
|
|
(defn format-url
|
|
"Format the Ring map as an url."
|
|
[m]
|
|
(if (not (empty? m))
|
|
(let [query-params (:query-params m)]
|
|
(str (if (:scheme m)
|
|
(str (name (:scheme m)) "://"))
|
|
(let [{:keys [username password]} m]
|
|
(when username
|
|
(str username (when password (str ":" password)) "@")))
|
|
(:server-name m)
|
|
(if-let [port (:server-port m)]
|
|
(if-not (= port (port-number (:scheme m)))
|
|
(str ":" port)))
|
|
(if (and (nil? (:uri m))
|
|
(not (empty? query-params)))
|
|
"/" (:uri m))
|
|
(if-not (empty? query-params)
|
|
(str "?" (format-query-params query-params)))
|
|
(if-not (blank? (:fragment m))
|
|
(str "#" (:fragment m)))))))
|
|
|
|
(defn public-url
|
|
"Return the formatted `url` without password as a string."
|
|
[url]
|
|
(format-url (dissoc url :password)))
|
|
|
|
(defn parse-percent
|
|
"Parse `s` as a percentage."
|
|
[s]
|
|
(parse-double (replace s "%" "")))
|
|
|
|
(defn pattern-quote
|
|
"Quote the special characters in `s` that are used in regular expressions."
|
|
[s]
|
|
(replace (name s) #"([\[\]\^\$\|\(\)\\\+\*\?\{\}\=\!.])" "\\\\$1"))
|
|
|
|
(defn separator
|
|
"Returns the first string that separates the components in `s`."
|
|
[s]
|
|
(if-let [matches (re-matches #"(?i)([a-z0-9_-]+)([^a-z0-9_-]+).*" s)]
|
|
(nth matches 2)))
|
|
|
|
(defn parse-query-params
|
|
"Parse the query parameter string `s` and return a map."
|
|
[s]
|
|
(if s
|
|
(->> (split (str s) #"&")
|
|
(map #(split %1 #"="))
|
|
(filter #(= 2 (count %1)))
|
|
(mapcat #(vector (keyword (url-decode (first %1))) (url-decode (second %1))))
|
|
(apply hash-map))))
|
|
|
|
(defn parse-url
|
|
"Parse the url `s` and return a Ring compatible map."
|
|
[s]
|
|
(if-let [matches (re-matches url-regex (str s))]
|
|
(let [scheme (keyword (nth matches 1))]
|
|
(compact-map
|
|
{:scheme scheme
|
|
:username (nth matches 3)
|
|
:password (nth matches 4)
|
|
:server-name (nth matches 6)
|
|
:server-port (or (parse-integer (nth matches 8)) (port-number scheme))
|
|
:uri (nth matches 10)
|
|
:query-params (parse-query-params (nth matches 12))
|
|
:query-string (nth matches 12)
|
|
:fragment (nth matches 14)}))))
|
|
|
|
(defmacro prog1 [& body]
|
|
"Evaluate `body`, returning the result of the first form."
|
|
`(let [result# ~(first body)]
|
|
~@(rest body)
|
|
result#))
|
|
|
|
(defn with-retries*
|
|
"Executes thunk. If an exception is thrown, will retry. At most n retries
|
|
are done. If still some exception is thrown it is bubbled upwards in
|
|
the call chain."
|
|
[n thunk]
|
|
(loop [n n]
|
|
(if-let [result
|
|
(try
|
|
[(thunk)]
|
|
(catch #?(:clj Exception :cljs js/Error) e
|
|
(when (zero? n)
|
|
(throw e))))]
|
|
(result 0)
|
|
(recur (dec n)))))
|
|
|
|
(defmacro with-retries
|
|
"Executes body. If an exception is thrown, will retry. At most n retries
|
|
are done. If still some exception is thrown it is bubbled upwards in
|
|
the call chain."
|
|
[n & body]
|
|
`(no.en.core/with-retries* ~n (fn [] ~@body)))
|
|
|
|
(defn- editable? [coll]
|
|
#?(:clj (instance? clojure.lang.IEditableCollection coll)
|
|
:cljs (satisfies? cljs.core.IEditableCollection coll)))
|
|
|
|
(defn- reduce-map [f coll]
|
|
(if (editable? coll)
|
|
(persistent! (reduce-kv (f assoc!) (transient (empty coll)) coll))
|
|
(reduce-kv (f assoc) (empty coll) coll)))
|
|
|
|
(defn map-keys
|
|
"Maps a function over the keys of an associative collection."
|
|
[f coll]
|
|
(reduce-map (fn [xf] (fn [m k v] (xf m (f k) v))) coll))
|
|
|
|
(defn map-vals
|
|
"Maps a function over the values of an associative collection."
|
|
[f coll]
|
|
(reduce-map (fn [xf] (fn [m k v] (xf m k (f v)))) coll))
|
|
|
|
(defn deep-merge
|
|
"Like merge, but merges maps recursively."
|
|
[& maps]
|
|
(if (every? map? maps)
|
|
(apply merge-with deep-merge maps)
|
|
(last maps)))
|
|
|
|
(defn deep-merge-with
|
|
"Like merge-with, but merges maps recursively, applying the given fn
|
|
only when there's a non-map at a particular level."
|
|
[f & maps]
|
|
(apply
|
|
(fn m [& maps]
|
|
(if (every? map? maps)
|
|
(apply merge-with m maps)
|
|
(apply f maps)))
|
|
maps))
|