Creating a parser combinator library to parse JSON
Prev: JSON arrays | Contents | Next: The finished result |
Both JSON arrays and objects have a list of things interspersed by commas; in the case of json-object
, it’s key-value pairs. In json-array
, it was JSON values that were separated by commas:
(optional (cons-seq forward.json-value (many (seq2 (skipwhite:match-is #\,) (must "a comma must be followed by a value" forward.json-value)))))
I can extract that pattern:
(def parse-intersperse (separator parser must-message) (optional (cons-seq parser (many (seq2 separator (must must-message parser))))))
The “must-message
” is the error message to give if we see a separator, but it isn’t followed by another thing that the parser matches.
A comma separated list is a specific case:
(def comma-separated (parser must-message) (parse-intersperse (skipwhite:match-is #\,) parser must-message))
Now json-array
is:
(= json-array (seq2 (match-is #\[) (comma-separated forward.json-value "a comma must be followed by a value") (must "a JSON array must be terminated with a closing ]" (skipwhite:match-is #\]))))
In a json-object
, key-value pairs are separated by a colon, and the key is always a JSON string:
(= json-object-kv (with-seq (key skipwhite.json-string colon (skipwhite:match-is #\:) value forward.json-value) (list key value)))
This matches a single key-value pair, and returns them as a two element list:
arc> (show-parse json-object-kv "\"abc\":[1,2,3]") returning: ("abc" (1 2 3)) remaining: nil
Arc’s listtab
will convert a list of those key-value pairs into a table for us:
(= json-object (on-result listtab (seq2 (match-is #\{) (comma-separated json-object-kv "comma must be followed by a key") (skipwhite:match-is #\}))))
(= json-value (skipwhite:alt json-true json-false json-null json-number json-string json-array json-object))
arc> (fromjson «{"a": [1, 2, {"b": 3}]}») #hash(("a" . (1 2 #hash(("b" . 3)) . nil)))
As usual, we can report errors better...
arc> (fromjson «{») not a JSON value: {arc> (fromjson «{"a"}») not a JSON value: {"a"}arc> (fromjson «{"a":}») not a JSON value: {"a":}
toss in a few must
’s:
(= json-object-kv (with-seq (key skipwhite.json-string colon (must "a JSON object key string must be followed by a :" (skipwhite:match-is #\:)) value (must "a colon in a JSON object must be followed by a value" forward.json-value)) (list key value)))
(= json-object (on-result listtab (seq2 (match-is #\{) (comma-separated json-object-kv "comma must be followed by a key") (must "a JSON object must be terminated with a closing }" (skipwhite:match-is #\})))))
and now we get:
arc> (fromjson «{») a JSON object must be terminated with a closing }arc> (fromjson «{"a"}») a JSON object key string must be followed by a :arc> (fromjson «{"a":}») a colon in a JSON object must be followed by a value
Prev: JSON arrays | Contents | Next: The finished result |
Questions? Comments? Email me andrew.wilcox [at] gmail.com