Skip to content

Commit 6126089

Browse files
authored
feat: transducer interface (#42)
* feat: tranducers interface * feat: support for custom jackson object mappers * feat: convenience transducer constructor * feat: serializer and parser transducers * feat: pretty-printer transducer * fix: copy the object mapper * refactor: transducer names as verbs * refactor: -> value * refactor: rename query->expression * refactor: compare with :cat and without directly
1 parent 9ab9c27 commit 6126089

File tree

4 files changed

+212
-10
lines changed

4 files changed

+212
-10
lines changed

Diff for: deps.edn

+4-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
:exclusions [org.slf4j/slf4j-log4j12
1010
org.slf4j/slf4j-api
1111
org.slf4j/slf4j-nop]}
12-
criterium/criterium {:mvn/version "0.4.6"}}}
12+
criterium/criterium {:mvn/version "0.4.6"}
13+
metosin/jsonista {:mvn/version "0.3.7"}}}
1314
:test
1415
{:extra-paths ["test" "test/resources"]
1516
:extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git"
16-
:sha "7284cda41fb9edc0f3bc6b6185cfb7138fc8a023"}}
17+
:sha "7284cda41fb9edc0f3bc6b6185cfb7138fc8a023"}
18+
metosin/jsonista {:mvn/version "0.3.7"}}
1719
:main-opts ["-m" "cognitect.test-runner"]}
1820
:clj-kondo
1921
{:main-opts ["-m" "clj-kondo.main" "--lint" "src" "test"]

Diff for: src/jq/api/api_impl.clj

+21-8
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,19 @@
1717

1818
(def jq-version Versions/JQ_1_6)
1919

20-
(defn ->JsonNode ^JsonNode [data]
21-
(if (instance? JsonNode data)
22-
data
23-
(.valueToTree mapper data)))
20+
(defn ->JsonNode
21+
(^JsonNode [data] (->JsonNode mapper data))
22+
(^JsonNode [^ObjectMapper mapper data]
23+
(if (instance? JsonNode data)
24+
data
25+
(.valueToTree mapper data))))
26+
27+
(defn JsonNode->clj
28+
"Converts JsonNode to a Clojure value.
29+
An optional object mapper can be passed to handle data types such as keywords."
30+
([^JsonNode json-node] (JsonNode->clj mapper json-node))
31+
([^ObjectMapper mapper ^JsonNode json-node]
32+
(.treeToValue mapper json-node ^Class Object)))
2433

2534
(defn ->absolute-path
2635
"FileSystemModuleLoader requires absolute paths."
@@ -64,11 +73,15 @@
6473
scope)
6574
old-scope))
6675

67-
(defn string->json-node ^JsonNode [^String data]
68-
(.readTree mapper data))
76+
(defn string->json-node
77+
(^JsonNode [^String data] (string->json-node mapper data))
78+
(^JsonNode [^ObjectMapper mapper ^String data]
79+
(.readTree mapper data)))
6980

70-
(defn json-node->string ^String [^JsonNode data]
71-
(.writeValueAsString mapper data))
81+
(defn json-node->string
82+
(^String [^JsonNode data] (json-node->string mapper data))
83+
(^String [^ObjectMapper mapper ^JsonNode data]
84+
(.writeValueAsString mapper data)))
7285

7386
; Helper interface that specifies a method to get a string value.
7487
(definterface IContainer

Diff for: src/jq/transducers.clj

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
(ns jq.transducers
2+
(:require [jq.api :as api]
3+
[jq.api.api-impl :as impl])
4+
(:import (com.fasterxml.jackson.databind ObjectMapper SerializationFeature)))
5+
6+
(defn ->JsonNode
7+
"Returns a transducer that given a Java object maps it to a JsonNode.
8+
Accepts an optional Jackson ObjectMapper."
9+
([] (map impl/->JsonNode))
10+
([^ObjectMapper mapper]
11+
(map (partial impl/->JsonNode mapper))))
12+
13+
(defn JsonNode->value
14+
"Returns a transducer that give a JsonNode maps it to a Java Object.
15+
Accepts an optional Jackson ObjectMapper."
16+
([] (map impl/JsonNode->clj))
17+
([^ObjectMapper mapper]
18+
(map (partial impl/JsonNode->clj mapper))))
19+
20+
(defn parse
21+
"Returns a transducer that given a JSON String parses it into a JsonNode.
22+
Accepts an optional Jackson ObjectMapper."
23+
([] (map impl/string->json-node))
24+
([^ObjectMapper mapper]
25+
(map (partial impl/string->json-node mapper))))
26+
27+
(defn serialize
28+
"Returns a transducer that given a JsonNode serializes it to a String.
29+
Accepts an optional Jackson ObjectMapper."
30+
([] (map impl/json-node->string))
31+
([^ObjectMapper mapper]
32+
(map (partial impl/json-node->string mapper))))
33+
34+
;; Pretty printer serializer
35+
(defn pretty-print
36+
"Same as the `serializer` but the output string is indented.
37+
ObjectMapper is copied to prevent side effects in case mapper is shared."
38+
([] (pretty-print impl/mapper))
39+
([^ObjectMapper mapper]
40+
(map (partial impl/json-node->string
41+
(.enable (.copy mapper) SerializationFeature/INDENT_OUTPUT)))))
42+
43+
(defn execute
44+
"Returns a transducer that accepts JsonNode on which
45+
the expression will be applied.
46+
Optional opts are supported that are passed for the `jq.api/stream-processor`.
47+
Specific opts for the transducer:
48+
:cat - whether to catenate output, default true"
49+
([^String expression] (execute expression {}))
50+
([^String expression opts]
51+
(let [xf (map (api/stream-processor expression opts))]
52+
(if (false? (:cat opts))
53+
xf
54+
(comp xf cat)))))
55+
56+
(defn process
57+
"Returns a convenience transducer that:
58+
- maps a Java Object to a JsonNode;
59+
- maps a JQ expression on the JsonNode;
60+
- catenates the output;
61+
- maps a JsonNode to a JavaObject.
62+
Accept opts that will be passed to the `jq.api/stream-processor`
63+
Accepts a Jackson ObjectMapper that will be used for both"
64+
([^String expression] (process expression {}))
65+
([^String expression opts] (process expression opts impl/mapper))
66+
([^String expression opts ^ObjectMapper mapper]
67+
(comp
68+
(->JsonNode mapper)
69+
(execute expression (assoc opts :cat true))
70+
(JsonNode->value mapper))))
71+
72+
(comment
73+
; Duplicates input
74+
(into []
75+
(comp
76+
(->JsonNode)
77+
(execute "(. , .)")
78+
(JsonNode->value))
79+
[1 2 3])
80+
81+
(into [] (process "(. , .)") [1 2 3]))

Diff for: test/jq/transducers_test.clj

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
(ns jq.transducers-test
2+
(:require [clojure.test :refer [deftest is testing]]
3+
[jq.transducers :as jq]
4+
[jsonista.core :as json]))
5+
6+
(deftest transducers-interface
7+
(let [data [1 2 3]
8+
expression ".+1"
9+
expected-result [2 3 4]]
10+
(testing "mapper"
11+
(is (= expected-result
12+
(sequence (comp
13+
(jq/->JsonNode)
14+
(jq/execute expression)
15+
(jq/JsonNode->value))
16+
data))))
17+
18+
(testing "string input is not parsed, it is converted to TextNode as is"
19+
(is (= ["test"]
20+
(sequence (comp
21+
(jq/->JsonNode)
22+
(jq/execute ".")
23+
(jq/JsonNode->value))
24+
["test"]))))
25+
26+
(testing "JSON string parser, it is converted to TextNode as is"
27+
(is (= [{"foo" "bar"}]
28+
(sequence (comp
29+
(jq/parse)
30+
(jq/execute ".")
31+
(jq/JsonNode->value))
32+
[(json/write-value-as-string {"foo" "bar"})]))))
33+
34+
(testing "string serializer"
35+
(is (= ["{\"a\":\"b\"}" "{\"a\":\"b\"}"]
36+
(sequence (comp
37+
(jq/->JsonNode)
38+
(jq/execute "(. , .)")
39+
(jq/serialize))
40+
[{"a" "b"}]))))
41+
42+
(testing "output catenation opt"
43+
(is (= (sequence (comp
44+
(jq/->JsonNode)
45+
(jq/execute expression)
46+
(jq/JsonNode->value))
47+
data)
48+
(sequence (comp
49+
(jq/->JsonNode)
50+
(jq/execute expression {:cat false})
51+
cat
52+
(jq/JsonNode->value))
53+
data))))
54+
55+
(testing "multiple scripts in a row"
56+
(is (= [3 4 5]
57+
(sequence (comp
58+
(jq/->JsonNode)
59+
(jq/execute expression)
60+
(jq/execute expression)
61+
(jq/JsonNode->value))
62+
data))))))
63+
64+
(deftest custom-mappers
65+
(let [data [{:a :b}]
66+
expression "."]
67+
(testing "default object mapper is bad at keywords"
68+
(is (= [{":a" {"name" "b"
69+
"namespace" nil
70+
"sym" {"name" "b"
71+
"namespace" nil}}}]
72+
(sequence (comp
73+
(jq/->JsonNode)
74+
(jq/execute expression)
75+
(jq/JsonNode->value))
76+
data))))
77+
(testing "keyword aware mappers"
78+
(let [mapper json/keyword-keys-object-mapper]
79+
(is (= [{:a "b"}]
80+
(sequence (comp
81+
(jq/->JsonNode mapper)
82+
(jq/execute expression)
83+
(jq/JsonNode->value mapper))
84+
data)))))
85+
86+
(testing "parser and serializer"
87+
(is (= ["{\"foo\":\"bar\"}" "{\"foo\":\"bar\"}"]
88+
(sequence (comp
89+
(jq/parse json/keyword-keys-object-mapper)
90+
(jq/execute "(. , .)")
91+
(jq/serialize json/keyword-keys-object-mapper))
92+
[(json/write-value-as-string {:foo "bar"})]))))
93+
94+
(testing "pretty serializer"
95+
(is (= ["{\n \"foo\" : \"bar\"\n}"]
96+
(sequence (comp
97+
(jq/->JsonNode)
98+
(jq/execute ".")
99+
(jq/pretty-print))
100+
[{"foo" "bar"}]))))))
101+
102+
(deftest convenience-transducer
103+
(let [data [1 2 3]
104+
expression "(. , .)"]
105+
(is (= [1 1 2 2 3 3]
106+
(sequence (jq/process expression) data)))))

0 commit comments

Comments
 (0)