forked from b-social/jason
-
Notifications
You must be signed in to change notification settings - Fork 1
/
core.clj
193 lines (156 loc) · 7.75 KB
/
core.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
(ns jason.core
"JSON encoding and decoding function construction with support for
configurable key conversion."
(:require
[clojure.string :refer [starts-with?]]
[jsonista.core :as jsonista]
[camel-snake-kebab.core
:refer [->camelCaseString
->snake_case_string
->kebab-case-keyword]]))
(def ^:dynamic *meta-prefix*
"Meta key prefix used to detect and preserve meta fields. Defaults to '_'."
"_")
(defn- is-meta-key? [k]
(starts-with? (name k) *meta-prefix*))
(defn- if-metadata
[meta-key-fn standard-key-fn]
(fn [k]
(if (is-meta-key? k)
(meta-key-fn k)
(standard-key-fn k))))
(defn- ->meta-key-fn [standard-key-fn]
(fn [k]
(let [converted (standard-key-fn (subs (name k) (count *meta-prefix*)))
finaliser (if (keyword? converted) keyword name)]
(finaliser (str *meta-prefix* (name converted))))))
(defn- ->key-fn
([default-key-fn] (->key-fn {} default-key-fn))
([options default-key-fn]
(let [standard-key-fn
(get options :standard-key-fn default-key-fn)
meta-key-fn
(get options :meta-key-fn (->meta-key-fn standard-key-fn))]
(if-metadata meta-key-fn standard-key-fn))))
(defn ->encode-key-fn
"Constructs a function to encode JSON keys.
With no arguments, encodes to camelCase strings and is _meta aware, i.e.,
any fields with leading meta prefix will retain their meta prefix.
With a function argument, uses that function to convert both standard and
meta keys while retaining leading meta prefix.
Also accepts a map option argument in place of the function which can contain:
- `:standard-key-fn`: the key function to use for standard fields, also
used for meta fields if no :meta-key-fn provided.
- `:meta-key-fn`: the key function to use for meta fields, overriding
all meta handling."
([] (->encode-key-fn {}))
([fn-or-opts]
(->key-fn
(if (map? fn-or-opts) fn-or-opts {:standard-key-fn fn-or-opts})
->camelCaseString)))
(defn ->decode-key-fn
"Constructs a function to decode JSON keys.
With no arguments, decodes to kebab-case keywords and is `_meta` aware, i.e.,
any fields with leading meta prefix will retain their meta prefix.
With a function argument, uses that function to convert both standard and
meta keys while retaining leading meta prefix.
Also accepts a map option argument in place of the function which can contain:
- `:standard-key-fn`: the key function to use for standard fields, also
used for meta fields if no `:meta-key-fn` provided.
- `:meta-key-fn`: the key function to use for meta fields, overriding
all meta handling."
([] (->decode-key-fn {}))
([fn-or-opts]
(->key-fn
(if (map? fn-or-opts) fn-or-opts {:standard-key-fn fn-or-opts})
->kebab-case-keyword)))
(defn new-object-mapper
"Constructs a Jackson `ObjectMapper`.
With no arguments, the returned object mapper encodes and decodes keys exactly
as provided, does not produce pretty JSON and includes no additional modules.
The optional first parameter is a map of options. The following options are
supported:
| Mapper options | |
| ------------------- | -------------------------------- |
| `:modules` | vector of `ObjectMapper` modules |
| Encoding options | |
| ------------------- | --------------------------------------------------- |
| `:pretty` | set to `true` use Jackson's pretty-printing defaults (default: `true`) |
| `:escape-non-ascii` | set to `true` to escape non-ASCII characters |
| `:date-format` | string for custom date formatting (default: `yyyy-MM-dd'T'HH:mm:ss'Z'`) |
| `:encode-key-fn` | `true` to coerce keyword keys to strings, `false` to leave them as keywords, or a function to provide custom coercion (default: the default of [[->encode-key-fn]]) |
| `:encoders` | a map of custom encoders where keys should be types and values should be encoder functions |
Encoder functions take two parameters: the value to be encoded and a
`JsonGenerator` object. The function should call `JsonGenerator` methods to
emit the desired JSON.
| Decoding options | |
| ------------------- | --------------------------------------------------- |
| `:decode-key-fn` | `true` to coerce keys to keywords, false to leave them as strings, or a function to provide custom coercion (default: the default of [[->decode-key-fn]]) |
| `:bigdecimals` | `true` to decode doubles as BigDecimals (default: `false`) |
See https://metosin.github.io/jsonista for further details of the underlying
JSON library, `jsonista`."
([] (new-object-mapper {}))
([options]
(jsonista/object-mapper options)))
(def ^:dynamic *default-object-mapper*
"Default ObjectMapper instance used when none provided. Has the same
configuration as when [[new-object-mapper]] is called with no argument."
(new-object-mapper))
(defn new-json-encoder
"Constructs a JSON encoder function. With no argument, uses the default
object mapper defined in [[*default-object-mapper*]]. Optionally, takes
an `ObjectMapper` to use instead.
The returned encoder returns nil on a nil value, otherwise JSON encodes it."
([] (new-json-encoder (new-object-mapper)))
([object-mapper]
(fn [value]
(when value
(jsonista/write-value-as-string value object-mapper)))))
(defn new-json-decoder
"Constructs a JSON decoder function. With no argument, uses the default
object mapper defined in [[*default-object-mapper*]]. Optionally, takes
an `ObjectMapper` to use instead.
The returned decoder returns nil on a nil or empty string value, otherwise
JSON decodes it."
([] (new-json-decoder (new-object-mapper)))
([object-mapper]
(fn [value]
(when value
(when-not (and (string? value) (empty? value))
(jsonista/read-value value object-mapper))))))
(defn new-json-coders
"Constructs a pair of JSON encode / decode functions, at keys `:->json` and
`:<-json` in the returned map.
With no arguments, uses the default `ObjectMapper` as returned by
[[new-object-mapper]]. The optional argument is the same map of options as
described in the documentation for [[new-object-mapper]]."
([] (new-json-coders {}))
([options]
(let [object-mapper (new-object-mapper options)]
{:->json (new-json-encoder object-mapper)
:<-json (new-json-decoder object-mapper)})))
(defmacro defcoders
"Defines a pair of JSON encode / decode functions in the current namespace.
If a symbol is passed as the first argument, it is used as the \"type\" of
the coders such that the defined functions have the names `-><type>-json` and
`<-<type>-json`.
If no symbol is passed, the defined coder functions have the names `->json`
and `<-json`.
In addition to a type symbol, a sequence of key-value arguments can be
provided, supporting the same options as exposed on [[new-json-coders]]
allowing full control over the generated functions."
([& args]
(let [coder-type-in-args? (odd? (count args))
coder-type (when coder-type-in-args? (first args))
{:as options} (if coder-type-in-args? (drop 1 args) args)
coder-symbol (fn [prefix]
(symbol (if coder-type-in-args?
(str (name prefix) (name coder-type) "-json")
(str (name prefix) "json"))))
encoder-symbol (coder-symbol :->)
decoder-symbol (coder-symbol :<-)]
`(let [{->json# :->json <-json# :<-json}
(new-json-coders ~options)]
(def ~encoder-symbol ->json#)
(def ~decoder-symbol <-json#)))))