Skip to content

Commit

Permalink
Cli param overrides with an embedded jq (#3)
Browse files Browse the repository at this point in the history
* chore: bump deps
* feat: added jq script to the query transformation pipeline
* feat: transforms for impact support jq scripts
* feat: support for jq config overrides
  • Loading branch information
dainiusjocas authored Mar 15, 2021
1 parent e902af4 commit 2a6116f
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 34 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ lint:
unit-test:
clojure -M:test --exclude :integration

.PHONY: check-deps
check-deps:
clojure -Sdeps '{:deps {antq/antq {:mvn/version "RELEASE"}}}' -M -m antq.core

.PHONY: integration-test
integration-test:
clojure -M:test --include :integration
Expand Down
30 changes: 16 additions & 14 deletions deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,47 @@
:mvn/repos
{"confluence" {:url "http://packages.confluent.io/maven/"}}
:deps
{org.clojure/clojure {:mvn/version "1.10.2"}
{org.clojure/clojure {:mvn/version "1.10.3"}
borkdude/sci {:mvn/version "0.2.1"}
borkdude/sci.impl.reflector {:mvn/version "0.0.1-java11"}
lt.jocas/lazy-elasticsearch-scroll {:mvn/version "1.0.16"}
lt.jocas/clj-jq {:mvn/version "1.0.0"
:exclusions [com.fasterxml.jackson.core/jackson-databind]}
http-kit/http-kit {:mvn/version "2.5.0"}
metosin/reitit {:mvn/version "0.5.10"}
io.confluent/kafka-connect-elasticsearch {:mvn/version "10.0.0"}
org.apache.kafka/connect-api {:mvn/version "2.6.0"}
org.apache.kafka/connect-json {:mvn/version "2.6.0"}
org.apache.kafka/connect-api {:mvn/version "2.7.0"}
org.apache.kafka/connect-json {:mvn/version "2.7.0"}
org.clojure/tools.logging {:mvn/version "1.1.0"}
org.clojure/tools.cli {:mvn/version "1.0.194"}
org.clojure/tools.cli {:mvn/version "1.0.206"}
org.clojure/core.async {:mvn/version "1.3.610"}
ch.qos.logback/logback-core {:mvn/version "1.2.3"}
ch.qos.logback/logback-classic {:mvn/version "1.2.3"}
ch.qos.logback.contrib/logback-json-classic {:mvn/version "0.1.5"}
ch.qos.logback.contrib/logback-jackson {:mvn/version "0.1.5"}
io.quarkus/quarkus-kafka-client {:mvn/version "1.9.2.Final"
io.quarkus/quarkus-kafka-client {:mvn/version "1.12.2.Final"
:exclusions [org.jboss.slf4j/slf4j-jboss-logging
org.jboss.logging/jboss-logging]}}
org.jboss.logging/jboss-logging
org.jboss.slf4j/slf4j-jboss-logmanager]}}
:aliases
{:dev
{:extra-paths ["dev" "classes" "test" "test/resources"]
:extra-deps {org.clojure/tools.deps.alpha {:git/url "https://github.com/clojure/tools.deps.alpha.git"
:sha "f6c080bd0049211021ea59e516d1785b08302515"
:exclusions [org.slf4j/slf4j-log4j12
org.slf4j/slf4j-api
org.slf4j/slf4j-nop]}
:extra-deps {org.clojure/tools.deps.alpha {:mvn/version "0.10.889"
:exclusions [org.slf4j/slf4j-log4j12
org.slf4j/slf4j-api
org.slf4j/slf4j-nop]}
criterium/criterium {:mvn/version "0.4.6"}}}
:test
{:extra-paths ["test" "test/resources"]
:extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git"
:sha "028a6d41ac9ac5d5c405dfc38e4da6b4cc1255d5"}}
:main-opts ["-m" "cognitect.test-runner"]}
:clj-kondo
{:main-opts ["-m" "clj-kondo.main --lint src test"]
{:main-opts ["-m" "clj-kondo.main" "--lint" "src" "test"]
:extra-deps {clj-kondo/clj-kondo {:mvn/version "2021.03.03"}}
:jvm-opts ["-Dclojure.main.report=stderr"]}
:native-ket
{:main-opts ["-m clj.native-image core"
{:main-opts ["-m" "clj.native-image" "core"
"--enable-https"
"--no-fallback"
"--language:js"
Expand All @@ -67,4 +69,4 @@
{:git/url "https://github.com/taylorwood/clj.native-image.git"
:exclusions [commons-logging/commons-logging
org.slf4j/slf4j-nop]
:sha "7708e7fd4572459c81f6a6b8e44c96f41cdd92d4"}}}}}
:sha "f3e40672d5c543b80a2019c1f07b2d3fe785962c"}}}}}
4 changes: 4 additions & 0 deletions graalvm/reflect-config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
[
{
"name": "java.lang.reflect.AccessibleObject",
"methods" : [{"name":"canAccess"}]
},
{
"name":"java.util.UUID",
"allPublicFields":true
Expand Down
9 changes: 9 additions & 0 deletions src/cli.clj
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,17 @@
[nil "--docs" "Print to STDOUT the docstring of the operation"
:default nil]
["-f" "--config-file CONFIG_FILE" "Path to the JSON file with operation config"]
[nil "--override OVERRIDE" "JQ scripts to be applied on the config file"
:multi true
:update-fn conj
:default []]
[nil "--dry-run" "After construction of config instead of executing the operation prints config to stdout and exits."
:default false]
["-h" "--help"]])

(comment
(cli/recursive-parse ["-f" "examples/replay.json" "--override" ".foo = 12" "--override" ".max_docs |= 13"] core/cli-operations))

(defn find-operation [operation-name operations]
(first (filter (fn [op] (= (name operation-name) (:name op))) operations)))

Expand Down
6 changes: 6 additions & 0 deletions src/cli/operation.clj
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@
[[nil "--defaults" "Print to STDOUT the default configuration of the operation"]
[nil "--docs" "Print to STDOUT the docstring of the operation"]
["-f" "--config-file CONFIG_FILE" "Path to the JSON file with operation config"]
[nil "--override OVERRIDE" "JQ scripts to be applied on the config file"
:multi true
:update-fn conj
:default []]
[nil "--dry-run" "After construction of config instead of executing the operation prints config to stdout and exits."
:default false]
["-h" "--help"]]))

(defn parse-opts [args defaults operation-name]
Expand Down
32 changes: 22 additions & 10 deletions src/core.clj
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
(ns core
(:require [clojure.java.io :as io]
[clojure.string :as str]
[clojure.tools.logging :as log]
[core.json :as json]
[cli :as cli]
[ops :as ops]
[ops-overrides :as ops-overrides]
[core.deep-merge :as dm])
[core.deep-merge :as dm]
[jq.core :as jq])
(:gen-class)
(:import (org.slf4j LoggerFactory)
(ch.qos.logback.classic Logger Level)))

(defn find-operation [operation-name cli-operations]
(first (filter (fn [op] (= (name operation-name) (:name op))) cli-operations)))

(defn read-config-file [config-file]
(defn read-config-file [config-file jq-overrides]
(if (and config-file (.exists (io/file config-file)))
(json/read-file config-file)
(if (seq jq-overrides)
(let [^String file-contents (slurp config-file)]
(json/decode (jq/execute file-contents (str/join " | " jq-overrides))))
(json/read-file config-file))
(do
(when config-file
(log/warnf "Config file '%s' does not exists" config-file))
Expand All @@ -39,19 +44,25 @@
(defn handle-subcommand [{:keys [options] :as cli-opts} cli-operations]
(try
(if-let [operation (get options :operation)]
(let [config-file (get options :config-file)
file-options (read-config-file config-file)]
(execute-op operation file-options cli-operations))
(let [{{operation-name :name
(let [dry-run? (:dry-run options)
config-file (get options :config-file)
file-options (read-config-file config-file (:override options))]
(if dry-run?
(println (json/encode file-options))
(execute-op operation file-options cli-operations)))
(let [dry-run? (:dry-run options)
{{operation-name :name
{:keys [options arguments summary errors]} :conf
:as my-op} :operation} cli-opts]
(if (seq errors)
(println errors)
(if (or (:help options) (empty? options))
(println (format "Help for '%s':\n" (name operation-name)) summary)
(let [configs-from-file (read-config-file (:config-file options))
combined-conf (dm/deep-merge configs-from-file options)]
(execute-op operation-name combined-conf cli-operations))))))
(let [configs-from-file (read-config-file (:config-file options) (:override options))
combined-conf (dm/deep-merge configs-from-file (dissoc options :override :config-file :dry-run))]
(if (or dry-run? (:dry-run options))
(println (json/encode combined-conf))
(execute-op operation-name combined-conf cli-operations)))))))
(catch Exception e
(println (format "Failed to execute with exception:\n '%s'" e))
(.printStackTrace e))))
Expand All @@ -68,6 +79,7 @@
(handle-subcommand cli-opts cli-operations)))))

(comment
(core/handle-cli ["--dry-run" "reindex" "-f" "config-file.json" "--override" ".foo = 12" "--override" ".max_docs |= 13"])
(core/handle-cli ["-o" "foo" "-f" "a"])
(core/handle-cli ["replay" "sink" "--connection.url=http://localhost:9200"])
(core/handle-cli ["replay" "sink" "-h"])
Expand Down
98 changes: 98 additions & 0 deletions src/polyglot/jq.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
(ns polyglot.jq
(:require [clojure.string :as str]
[core.json :as json]
[jq.core :as jq]))

(defn script->transform-fn
"Given a JQ script snippet creates a function that expects a JSON
string that is going to be passed into that function as an argument.
Returns a string."
[^String script]
(jq/processor script))

(defn script->transform-fn-vals
"Given a JQ script snippet creates a function that accepts any number
of arguments where first argument must be a JSON string and the rest
of the arguments can be of any type. The output of is a JSON string."
[^String script]
(let [jq-fn (jq/processor script)]
(fn [& args]
(jq-fn (str "[" (str/join "," (concat [(first args)] (map json/encode-vanilla (rest args)))) "]")))))

(comment
;; Add an foo attribute with value bar to a map
(let [tf (polyglot.jq/script->transform-fn ".foo = \"bar\"")]
(tf "{}"))
;; => {"foo":"bar"}

;; String interpolation
(let [tf (polyglot.jq/script->transform-fn ".a = \">>\\(.a+10)<<\"")]
(tf "{\"a\": 12}"))
;; => {"a":">>22<<"}

;; Generate an array
(let [tf (polyglot.jq/script->transform-fn "[(.a,.b)=range(3)]")]
(tf "{\"a\": 12}"))
;; => [{"a":0,"b":0},{"a":1,"b":1},{"a":2,"b":2}]


;; Collect all the keys from an array of maps to an array
(let [tf (polyglot.jq/script->transform-fn-vals "[.[] | keys] | add")]
(tf "{\"a\": 1}" {:b 2}))
;; => ["a","b"]

;; Assoc attribute e with the value of second arg to the first argument
(let [tf (polyglot.jq/script->transform-fn-vals ". as [$first_arg, $second_arg] | $first_arg | .e = $second_arg")]
(tf "{\"a\": 1}" {:b 2}))
;; => {"a":1,"e":{"b":2}}

;; Work with an array of elements
(let [tf (polyglot.jq/script->transform-fn-vals
".[0] as $first_arg | .[1] as $second_arg | {} | .\"1st\" = $first_arg | .\"2nd\" = $second_arg")]
(tf "{\"a\": 1}" "{\"b\": 2}"))
;; => {"1st":{"a":1},"2nd":{"b":2}}

;; Example of destructuring an array
(let [tf (polyglot.jq/script->transform-fn-vals
". as [$first_arg, $second_arg] | {} | .fa = $first_arg | .sa = $second_arg")]
(tf "{\"a\": 1}" "{\"b\": 2}"))
;; => {"fa":{"a":1},"sa":{"b":2}}

;; Recursively collect all the 'a' attributes from an array of maps
;; and remove those records that value is null
(let [tf (polyglot.jq/script->transform-fn-vals
"[.. | .a?] | map(select(. != null))")]
(tf "{\"a\": 1}" "{\"b\": 2}"))
;; => [1]

; Collect al; the paths from a nested map
(let [tf (polyglot.jq/script->transform-fn-vals
"select(objects)|=[.] | map( paths(scalars) ) | map( map(select(numbers)=\"[]\") | join(\".\")) | unique")]
(tf "{\"a\": 1, \"b\": {\"c\": 3}}"))
;; => ["a","b.c"]

;; Collect all paths from an array
(let [tf (polyglot.jq/script->transform-fn-vals
"select(objects)|=[.] | map( paths(scalars) ) | map( map(select(numbers)=\"[]\") | join(\".\")) | unique")]
(tf "[{\"a\": 1, \"b\": {\"c\": 3}}]"))
;; => ["[].a","[].b.c"]

;; With plain values
(let [tf (polyglot.jq/script->transform-fn-vals
". as [$first_arg, $second_arg, $third_arg] | {} | .fa = $first_arg | .sa = $second_arg | .ta = $third_arg")]
(tf "{\"a\": 1}" 11 "\"string\""))
;; => {"fa":{"a":1},"sa":11,"ta":"string"}

;; All types of values can be passed to the function as args
(let [tf (polyglot.jq/script->transform-fn-vals
". as [$first_arg, $second_arg, $third_arg, $fourth_arg, $fifth_arg, $sixth_arg]
| {}
| .\"1st\" = $first_arg
| .\"2dn\" = $second_arg
| .\"3rd\" = $third_arg
| .\"4th\" = $fourth_arg
| .\"5th\" = $fifth_arg
| .\"6th\" = $sixth_arg")]
(tf "{\"a\": 1}" "my_string" 12 false nil {:b 10}))
;; => {"1st":{"a":1},"2dn":"my_string","3rd":12,"4th":false,"5th":null,"6th":{"b":10}}
)
6 changes: 5 additions & 1 deletion src/replay/transform/impact.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(ns replay.transform.impact
(:require [polyglot.sci :as sci]
[polyglot.js :as js]
[polyglot.jq :as jq]
[core.json :as json]))

(defn cart [colls]
Expand All @@ -27,6 +28,7 @@
(assoc transformation :fn (case (keyword (:lang transformation))
:js (js/script->transform-fn-vals (:script transformation))
:sci (sci/sci-compile (:script transformation))
:jq (jq/script->transform-fn-vals (:script transformation))
(throw (Exception. (format "Language code '%s' is not supported"
(:lang transformation)))))))
transformations))
Expand All @@ -42,6 +44,7 @@
; js transformation always returns a string
; we need to decode the response string
:js (json/decode (afn (json/encode acc) aval))
:jq (json/decode (afn (json/encode acc) aval))
(throw (Exception. (format "Language code '%s' is not supported" lang)))))
query-map
fns-to-apply)})) transform-variations))
Expand Down Expand Up @@ -69,4 +72,5 @@
[{:lang :sci :id :a :script "(fn [query value] (assoc query :a value))" :vals [1 2 3]}
{:lang :sci :id :b :script "(fn [query value] (assoc query :b value))" :vals [10 20 30]}
{:lang :sci :id :c :script "(fn [query value] (assoc query :c value))" :vals [100 200 300]}
{:lang :js :id :d :script "(query, value) => { query['d'] = value; return query; }" :vals ["a" "b" "c"]}]))
{:lang :js :id :d :script "(query, value) => { query['d'] = value; return query; }" :vals ["a" "b" "c"]}
{:lang :jq :id :d :script ". as [$query, $value] | $query | .e = $value" :vals ["X" "Y" "Z"]}]))
32 changes: 23 additions & 9 deletions src/replay/transform/query.clj
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
(ns replay.transform.query
(:require [polyglot.js :as js]
[polyglot.jq :as jq]
[polyglot.sci :as sci]))

(defn compile-transform [{:keys [lang script]}]
(case (keyword lang)
:sci (sci/script->transform-fn script)
:js (js/script->transform-fn script)
:jq (jq/script->transform-fn script)
(throw (Exception. (format "No such language supported: '%s'" (name lang))))))

;; TODO: optimize consecutive JQ scripts to be executed in one pass
(defn transform-fn [transforms]
(let [tf-fn (apply comp (map compile-transform (reverse transforms)))]
(fn [query] (tf-fn query))))

(comment
(time
(let [data "{}"
tfs [{:lang :js
:script "(request) => request"}
{:lang :sci
:script "(fn [q] (assoc q :_explain true))"}]
tf (transform-fn tfs)]
(dotimes [i 1000]
(tf data)))))
;; Applies transforms on the input string in order
(let [data "{}"
tfs [{:lang :js
:script "(request) => { request['_source'] = false; return request; }"}
{:lang :sci
:script "(fn [q] (assoc q :_explain true))"}
{:lang :jq
:script ".size = 15"}]
tf (transform-fn tfs)]
(tf data))
;; => {"_explain":true,"_source":false,"size":15}

;; JQ scripts are the fastest option to transform
(let [data "{\"a\": 12}"
tfs [{:lang :jq
:script ".foo |= \"bar\""}]
tf (transform-fn tfs)]
(tf data))
;; => {"a":12,"foo":"bar"}
)
Loading

0 comments on commit 2a6116f

Please sign in to comment.