Coverage report: /home/ellis/comp/core/lib/dat/toml.lisp
Kind | Covered | All | % |
expression | 0 | 584 | 0.0 |
branch | 0 | 42 | 0.0 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;; dat/toml.lisp --- TOML
3
;; TOML de/serialization for Lisp.
7
;; This code was originally based on https://github.com/sheepduke/clop which
8
;; provides a TOML parser using the ESRAP package.
10
;; ref: https://toml.io/en/v1.0.0
12
;; grammar: https://raw.githubusercontent.com/toml-lang/toml/1.0.0/toml.abnf
15
* TOML is case-sensitive.
16
* A TOML file must be a valid UTF-8 encoded Unicode document.
17
* Whitespace means tab (0x09) or space (0x20).
18
* Newline means LF (0x0A) or CRLF (0x0D 0x0A).
22
(in-package :dat/toml)
26
"The value of +inf when decoding TOML.")
29
"The value of -inf when decoding TOML.")
32
"The value of +nan when decoding TOML.")
35
"The value of -nan when decoding TOML.")
37
(defclass toml-object (ast) ())
39
(defmethod print-object ((self toml-object) stream)
40
(print-unreadable-object (self stream :type t)
41
(format stream "~A" (car (ast self)))))
43
(defclass toml-table (toml-object) ())
45
(defclass toml-document (toml-object) ())
48
(defun toml-peek-char (stream expected &key skip-ws)
49
(when (equal (peek-char skip-ws stream nil) expected)
52
(defun toml-read-char (stream expected &key skip-ws)
53
(declare (optimize (speed 3) (debug 0)))
54
(if (toml-peek-char stream expected :skip-ws skip-ws)
56
(error "TOML error: unexpected ~s~%expected ~A" (read-char stream) expected)))
58
(defun toml-read (stream &optional (eof-error-p t) eof-value)
59
(let ((c (peek-char t stream eof-error-p :eof)))
62
(#\[ (toml-read-table stream)) ;; arrays are values only
63
(#\# (toml-read-comment stream) (toml-read stream eof-error-p eof-value))
64
(t (toml-read-pair stream)))))
66
(defun toml-read-table (stream)
67
(toml-read-char stream #\[ :skip-ws t) ; [
68
(let ((ret (toml-read-header stream)))
69
(loop while (or (toml-read-comment stream) (toml-peek-bare-char stream))
70
do (push (toml-read-pair stream) ret))
71
(make-instance 'toml-table :ast (nreverse ret))))
73
(defun toml-read-header (stream)
74
(let ((c (peek-char t stream nil nil))
78
(#\[ (read-char stream)
79
(push (list (toml-read-key stream)) ret)
80
(toml-read-char stream #\]))
81
(t (push (toml-read-key stream) ret)))
82
(toml-read-char stream #\])
85
(defun toml-bare-char-p (c)
86
(or (digit-char-p c) (sb-unicode:alphabetic-p c) (char= #\- c) (char= #\_ c)))
88
(defun toml-peek-bare-char (stream)
89
(let ((c (peek-char t stream nil nil)))
90
(and c (toml-bare-char-p c))))
92
(defun toml-read-bare-key (stream)
93
(with-output-to-string (s)
94
(loop for c = (peek-char t stream nil nil)
95
while (and c (toml-bare-char-p c))
96
do (write-char (read-char stream) s))
99
(defun toml-read-string (stream)
100
"TOML supports basic, multi-line basic, literal, and multi-line literal
101
strings. All strings are UTF-8."
102
(let ((q (read-char stream)))
103
(with-output-to-string (s)
104
(if (eql q (peek-char t stream nil nil)) ;; 2 quotes
105
(if (eql q (peek-char t stream nil nil)) ;; 3 quotes (multi-line)
107
;; first we consume the first 2 multi-line quote chars
108
(read-sequence (make-string 2) stream)
109
(loop for c = (read-char stream nil nil)
112
do (let ((c1 (read-char stream nil nil))
113
(c2 (read-char stream nil nil)))
114
(if (char= q c1 c2) ;; 3 quotes
120
else do (write-char c s)))
121
;; empty string (2 quotes)
123
(read-char stream nil nil) ;; q
125
(loop for c = (read-char stream nil nil)
129
else do (write-char c s))))))
131
(defun toml-read-simple-string (stream)
132
"Read a single-quoted string."
133
(let ((q (read-char stream)))
134
(with-output-to-string (s)
135
(loop for c = (read-char stream nil nil)
136
until (or (eql c q) (not c))
140
(defun toml-read-key (stream)
141
"TOML supports bare, quoted, and dotted keys."
142
(let ((c (peek-char t stream nil nil)))
146
(let ((key (toml-read-simple-string stream)))
147
(if (toml-peek-char stream #\.)
148
(cons key (toml-read-key stream))
151
(t (let ((key (toml-read-bare-key stream)))
152
(unless (sequence:emptyp key)
153
(if (toml-peek-char stream #\.)
154
(cons key (toml-read-key stream))
157
(defun toml-read-pair (stream)
158
(when-let ((key (toml-read-key stream)))
159
(toml-read-char stream #\= :skip-ws t)
160
(cons key (toml-read-value stream))))
162
(defun toml-read-value (stream)
163
(let ((c (peek-char t stream nil nil)))
165
(#\{ (toml-read-inline-table stream))
166
((or #\" #\') (toml-read-string stream))
167
(#\[ (toml-read-array stream))
168
(#\t (toml-read-true stream))
169
(#\f (toml-read-false stream))
170
(t (toml-read-number-or-datetime stream)))))
172
(defun toml-read-inline-table (stream)
173
(toml-read-char stream #\{ :skip-ws t) ; {
176
(toml-peek-char stream #\, :skip-ws t)
177
(if (toml-peek-char stream #\} :skip-ws t)
179
(if-let ((pair (toml-read-pair stream)))
181
(return (nreverse ret)))))))
183
(defun toml-read-array (stream)
184
(toml-read-char stream #\[ :skip-ws t)
187
(if (progn (toml-peek-char stream #\, :skip-ws t)
188
(toml-peek-char stream #\] :skip-ws t))
189
(return (coerce ret 'vector))
190
(push (toml-read-value stream) ret)))))
192
(defun toml-read-true (stream)
193
(let ((s (make-string 4)))
194
(read-sequence s stream)
197
(error "TOML error: expected 'true', got ~A" s))))
199
(defun toml-read-false (stream)
200
(let ((s (make-string 5)))
201
(read-sequence s stream)
202
(if (equal s "false")
204
(error "TOML error: expected 'false', got ~A" s))))
206
(defun toml-read-number-or-datetime (stream)
207
(let ((c (peek-char t stream nil nil)))
209
(#\+ (read-char stream) (toml-read-positive stream))
210
(#\- (read-char stream) (toml-read-negative stream))
211
(#\n (toml-read-nan stream))
212
(#\i (toml-read-inf stream))
213
(t (let ((n (read stream)))
215
;; junk allowed for parsing time values
216
(if-let ((%n (ignore-errors (parse-number n))))
218
;; if we can't parse as a number try it as a datetime
219
(toml-parse-datetime stream))
222
(defun toml-read-positive (stream)
223
(let ((c (peek-char t stream nil nil)))
225
(#\i (toml-read-inf stream) *+inf*)
226
(#\n (toml-read-nan stream) *+nan*)
227
(t (abs (parse-number (read stream)))))))
229
(defun toml-read-negative (stream)
230
(let ((c (peek-char t stream nil nil)))
232
(#\i (toml-read-inf stream) *-inf*)
233
(#\n (toml-read-nan stream) *-nan*)
234
(t (- (parse-number (read stream)))))))
236
(defun toml-read-nan (stream)
237
(let ((s (make-string 3)))
238
(read-sequence s stream)
241
(error "TOML error: expected 'nan', got ~A" s))))
243
(defun toml-read-inf (stream)
244
(let ((s (make-string 3)))
245
(read-sequence s stream)
248
(error "TOML error: expected 'inf', got ~A" s))))
250
(defun toml-read-comment (stream)
251
(loop while (toml-peek-char stream #\# :skip-ws t)
252
do (read-line stream)))
254
;; TODO 2024-12-23: may include spaces, can't do a simple read :C
255
(defun toml-parse-datetime (str)
256
(or (ignore-errors (parse-timestring (ppcre:regex-replace " " "T" str)))
257
(toml-parse-datetime-local str)))
259
(defmethod toml-parse-datetime-local (str)
260
(let* ((delimeter (sequence:elt str 10))
261
(splits (split-sequence delimeter str)))
262
(append (with-input-from-string (s (car splits)) (toml-parse-date-local s))
263
(with-input-from-string (s (cadr splits)) (toml-parse-time-local (cadr splits))))))
265
(defun toml-parse-date-local (value)
266
"Return a plist with :year :month :date."
267
(let* ((*default-timezone* +utc-zone+)
268
(timestamp (parse-timestring value)))
269
(list :year (timestamp-year timestamp)
270
:month (timestamp-month timestamp)
271
:day (timestamp-day timestamp))))
273
(defun toml-parse-time-local (value)
274
"Return a plist with :hour :minute :second."
275
(let* ((*default-timezone* +utc-zone+)
276
(timestamp (parse-timestring value)))
277
(list :hour (timestamp-hour timestamp)
278
:minute (timestamp-minute timestamp)
279
:second (timestamp-second timestamp)
280
:microsecond (timestamp-microsecond timestamp))))
282
;; TODO 2024-12-24: traverse?
283
(defmethod unwrap ((self toml-document))
284
(mapcar 'ast (ast self)))
289
;; (deserialize "[test]" :toml)
290
;; (serialize '("test") :toml)
292
(defmethod serialize ((table toml-table) (format (eql :toml)) &key (style :alist))
293
(loop for k in (ast table)
294
collect (cons k (serialize (cdr k) format :style style))))
296
(defun toml-read-document (stream)
297
(make-instance 'toml-document
299
(loop for x = (toml-read stream nil nil)
303
(defmethod deserialize ((from pathname) (format (eql :toml)) &key)
304
(with-open-file (f from)
305
(toml-read-document f)))
307
(defmethod deserialize ((from string) (format (eql :toml)) &key)
308
(with-input-from-string (s from)
309
(toml-read-document s)))
311
(defmethod deserialize ((from stream) (format (eql :toml)) &key)
312
(with-open-stream (s from)
313
(toml-read-document s)))