-
Notifications
You must be signed in to change notification settings - Fork 3
/
functions.lisp
327 lines (302 loc) · 12.3 KB
/
functions.lisp
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
;;;; SPDX-FileCopyrightText: Atlas Engineer LLC
;;;; SPDX-License-Identifier: BSD-3-Clause
(in-package #:njson)
;; FIXME: CL pathname parsing on SBCL+Linux parses #p"/i\\j" as
;; #p"/ij", which breaks one of JSON Pointer RFC example. It's bad,
;; but avoidable with e.g. `make-pathname'.
(defun parse-pointer-pathname (pointer-pathname)
"Parse POINTER-PATHNAME per JSON Pointer rules (https://www.rfc-editor.org/rfc/rfc6901).
Only supports JSON string representation, not the URL one."
(flet ((resolve-tildes (string)
(uiop:frob-substrings
string '("~1" "~0")
(lambda (match frob)
(funcall frob (case (elt match 1)
(#\1 "/")
(#\0 "~"))))))
(read-until (char stream)
"Read from STREAM until encountering CHAR.
CHAR is left unread on STREAM after returning."
(coerce (loop for peeked = (peek-char nil stream nil nil)
until (or (eql char peeked)
(null peeked))
collect (read-char stream nil nil))
'string))
(parse-if-number (string)
(if (and (not (uiop:emptyp string))
(every #'digit-char-p string))
(parse-integer string)
string)))
(let* ((name (namestring pointer-pathname)))
(restart-case
(with-input-from-string (s name)
(loop for char = (read-char s nil nil)
while char
unless (eq #\/ char)
do (cerror "Use the pointer anyway"
'invalid-pointer :pointer pointer-pathname)
collect (parse-if-number (resolve-tildes (read-until #\/ s)))))
(another-pointer (new-pointer)
:report "Parse another pointer"
:interactive read-new-pointer
(parse-pointer-pathname new-pointer))))))
;; TODO: Merge this into `jget' in 2.*.
(defgeneric jget* (key-or-index object)
(:method ((keys sequence) (object t))
(case (length keys)
(0 (values object t))
(1 (jget* (elt keys 0) object))
(t (jget* (subseq keys 1)
(jget* (elt keys 0) object)))))
(:method ((index integer) (object array))
(cond
((<= 0 index (1- (length object)))
(values (aref object index) t))
(t (restart-case
(cerror "Return nothing"
'no-key :object object :key index)
(store-value (new-value)
:report "Add a value under this key"
:interactive read-new-value
(adjust-array object index)
(setf (elt object index) new-value)
(values new-value t))))))
(:method ((key string) (object hash-table))
(cond
((nth-value 1 (gethash key object))
(gethash key object))
(t (restart-case
(cerror "Return nothing"
'no-key :object object :key key)
(store-value (new-value)
:report "Add a new value under this key"
:interactive read-new-value
(setf (gethash key object) new-value)
(values new-value t))))))
(:method ((pointer pathname) object)
(if (equal #p"" pointer)
(values object t)
(jget* (parse-pointer-pathname pointer) object)))
(:method ((index string) (object array))
(restart-case
(cerror "Return nothing"
'invalid-key :key index :object object)
(coerce-to-integer ()
:report "Convert the key to integer"
:test (lambda (c)
(declare (ignore c))
(every #'digit-char-p index))
(jget* (parse-integer index) object))
(use-integer (new-index)
:report "Use an integer key"
:interactive read-new-key
(check-type new-index integer)
(jget* new-index object))))
(:method ((key integer) (object hash-table))
(restart-case
(cerror "Return nothing"
'invalid-key :key key :object object)
(coerce-to-string ()
:report "Convert the index to string"
(jget* (princ-to-string key) object))
(use-string (new-key)
:report "Use a string key"
:interactive read-new-key
(check-type new-key string)
(jget* new-key object))))
(:method (key object)
(declare (ignore key))
(cerror "Return nothing"
'non-indexable :value object)
(values nil nil))
(:method ((key string) (object string))
(declare (ignore key))
(cerror "Return nothing"
'non-indexable :value object))
(:method ((key integer) (object string))
(declare (ignore key))
(cerror "Return nothing"
'non-indexable :value object))
(:documentation "A version of `jget' that's more strict regarding missing keys."))
(defgeneric jget (key-or-index object)
(:method (key-or-index object)
(handler-case
(jget* key-or-index object)
(no-key ()
(values nil nil))))
(:documentation "Get the value at KEY-OR-INDEX in OBJECT.
KEY-OR-INDEX can be
- an integer (for array indexing),
- a string (for object keying),
- a pathname (with JSON Pointer syntax),
- a sequence of integers and strings (to index the nested structures).
- an empty sequence/pathname (to match the whole object).
Return two values: the value under KEY-OR-INDEX and whether this value
was found.
- (Starting from version 2) Throw `no-key' when the key is not present in the object.
- Throw `invalid-key' if using the wrong index type.
- Throw `non-indexable' when trying to index something other than
JSON arrays or objects.
- Throw `invalid-pointer' when using JSON Pointer with invalid syntax
as key.
For example, to get the data from a structure like
{\"data\": [1, 2, {\"three\": 3}]}
you can use
(jget #(\"data\" 2 \"three\") data)
;; => 3, T
OBJECT can be JSON array or object, which in Lisp translates to
`array' or `hash-table'.
`jget*' is a more structured and strict version of `jget', enforcing
the `no-key' condition and removing the two-valued approach because of
that. `jget*' will be merged into `jget' in version 2."))
(defgeneric (setf jget) (value key-or-index object)
(:method (value (keys sequence) (object t))
(case (length keys)
(0 (cerror "Don't set the value"
'invalid-key :key keys :object object))
(1 (setf (jget (elt keys 0) object) value))
(t (setf (jget (elt keys (1- (length keys)))
(jget (subseq keys 0 (1- (length keys))) object))
value))))
(:method (value (index integer) (object array))
(setf (aref object index) value))
(:method (value (key string) (object hash-table))
(setf (gethash key object) value))
(:method (value (pointer pathname) object)
(if (equal #p"" pointer)
(restart-case
(cerror "Don't set the value"
'invalid-key :key pointer :object object)
(another-pointer (new-pointer)
:report "Use another pointer"
:interactive read-new-pointer
(setf (jget new-pointer object) value)))
(setf (jget (parse-pointer-pathname pointer) object)
value)))
(:method (value (index string) (object array))
(restart-case
(cerror "Don't set the value"
'invalid-key :key index :object object)
(use-integer (new-key)
:report "Use an integer key"
:interactive read-new-key
(check-type new-key integer)
(setf (jget new-key object) value))) )
(:method (value (key integer) (object hash-table))
(restart-case
(cerror "Don't set the value"
'invalid-key :key key :object object)
(use-string (new-key)
:report "Use a string key"
:interactive read-new-key
(check-type new-key string)
(setf (jget new-key object) value))))
(:method (value key (object t))
(declare (ignore value key))
(cerror "Don't set the value"
'non-indexable :value object))
(:method :around (value key (object string))
(declare (ignore value key))
(cerror "Do nothing"
'non-indexable :value object))
(:documentation "Set the value at KEY-OR-INDEX in OBJECT.
The arguments are the same as in `jget', except KEY-OR-INDEX cannot be
an empty pathname/sequence (because setting the object itself to a new
value is not possible in CL, unless it's a place, which is not
guaranteed for `jget' arguments).
- Throw `invalid-key' if using the wrong index type.
- Throw `non-indexable' when trying to index something other than
JSON arrays or objects.
- Throw `invalid-pointer' when using JSON Pointer with invalid syntax
as key.
OBJECT can be JSON array or object, which in Lisp translates to
`array' or `hash-table'."))
(defgeneric jcopy (object)
(:method ((object real)) object)
(:method ((object (eql :null))) object)
(:method ((object (eql t))) object)
(:method ((object null)) object)
(:method ((object string)) object)
(:method ((object array))
(make-array (length object)
:adjustable t
:fill-pointer t
:initial-contents (map 'vector #'jcopy object)))
(:method ((object hash-table))
(let ((new (make-hash-table :test 'equal)))
(maphash (lambda (key val)
(setf (gethash key new) val))
object)
new))
(:documentation "Copy the OBJECT, potentially creating an identical one.
Coerce all JSON arrays to adjustable vectors."))
(defgeneric jkeys (object)
(:method ((object vector))
(loop for i from 0 below (length object)
collect i))
(:method ((object string))
(cerror "Return nothing"
'non-indexable :value object))
(:method ((object hash-table))
(loop for key being the hash-key of object
collect key))
(:method ((object t))
(cerror "Return nothing"
'non-indexable :value object))
(:documentation "Get keys to index OBJECT with, as a list of integers/strings.
If the OBJECT is not a JSON array/object, throws `non-indexable'."))
(defgeneric jtruep (object)
(:method (object)
(declare (ignore object))
t)
(:method ((object symbol))
(not (member object (list nil :null))))
(:documentation "Test OBJECT for truthiness in JSON terms.
Recognize all the values true, except for null and false. This is to
make the transition from JSON to Lisp (2 false values -> 1 false
value) smoother.
Unlike JavaScript, empty strings and zero are not false (because this
behavior is confusing)."))
(dolist (symbol '(jtrue-p jtrue?))
(setf (symbol-function symbol) #'jtruep))
(defun jnot (arg)
"JSON-aware version of `cl:not'."
(not (jtruep arg)))
(defun make-singular-array (object)
(make-array 1 :adjustable t :fill-pointer t :initial-contents (list object)))
(defgeneric ensure-array (object &key &allow-other-keys)
(:method ((object hash-table) &key convert-objects &allow-other-keys)
(if convert-objects
(make-array (hash-table-count object)
:adjustable t
:fill-pointer t
:initial-contents (loop for key in (jkeys object)
collect (jget key object)))
(make-singular-array object)))
(:method ((object sequence) &key &allow-other-keys)
(make-array (length object) :adjustable t :fill-pointer t :initial-contents object))
(:method ((object string) &key &allow-other-keys)
(make-singular-array object))
(:method ((object null) &key &allow-other-keys)
(make-singular-array object))
(:method ((object t) &key &allow-other-keys)
(make-singular-array object))
(:documentation "Ensure that the return value is an array.
If OBJECT is an array already, return it.
If it's a literal value, wrap it into a one-element array.
If it's an object:
- When CONVERT-OBJECTS is T, put all the values into an array (order
not guaranteed).
- Otherwise wrap the object into an array."))
(defgeneric ensure-object (key object &key &allow-other-keys)
(:method ((key string) (object hash-table) &key &allow-other-keys)
(jget key object)
object)
(:method ((key string) (object t) &key &allow-other-keys)
(let ((hash-table (make-hash-table :test 'equal)))
(setf (jget key hash-table) object)
hash-table))
(:documentation "Ensure that the return value is a JSON object.
If OBJECT is an object already, return it, checking KEY presence.
If it's anything else, wrap it into an object with OBJECT under KEY.
Throws errors from underlying `jget'."))