Herodotus
A Common Lisp json library for CLOS classes
Install / Use
/learn @HenryS1/HerodotusREADME
[[https://github.com/HenryS1/herodotus/tree/master][https://github.com/HenryS1/herodotus/actions/workflows/ci.yaml/badge.svg]]
- Herodotus
** Installation
The library is available on quicklisp since the ~2021-04-11~ dist so you can install with #+begin_src lisp (ql:quickload :herodotus) #+end_src
To install from source clone the repo into your lisp sources directory and load with asdf #+begin_src lisp (asdf:load-system :herodotus) #+end_src or use [[https://github.com/fukamachi/qlot][qlot]] #+begin_src txt git herodotus https://github.com/HenryS1/herodotus.git :branch master #+end_src
** Basic usage
This library provides a macro that generates JSON serialisation and deserialisation at the same time as defining a CLOS class.
For example the below definition creates a CLOS class with two fields ~x~ and ~y~ which have obvious accessors (~x~ and ~y~) and initargs (~:x~ and ~:y~). #+begin_src lisp CL-USER> (herodotus:define-json-model point (x y)) #+end_src
It also defines a package named ~point-json~ with a function for parsing json ~point-json:from-json~ and also implements a generic method for writing to json in the herodotus package.
#+begin_src lisp CL-USER> (defvar point (point-json:from-json "{ "x": 1, "y": 2 }")) POINT CL-USER> (x point) 1 CL-USER> (y point) 2 CL-USER> (herodotus:to-json point) "{"x":1,"y":2}" #+end_src
** Nested classes
You can also define classes that have class members using the type specifier syntax. This block defines two json models ~tree~ and ~branch~. A ~tree~ has ~branch~ members and the branch members will be parsed from json using the parser defined for the ~tree~.
#+begin_src lisp CL-USER> (herodotus:define-json-model branch (size)) CL-USER> (herodotus:define-json-model tree ((branches branch))) #+end_src
The syntax ~(branches branch)~ declares that the field named ~branches~ must be parsed as the type ~branch~. Json models for nested classes need to be defined before the models for the classes they are nested in or an error will be thrown. The error is thrown at macro expansion time.
#+begin_src lisp CL-USER> (herodotus:define-json-model test-no-parser ((things not-parseable))) CL-USER> (herodotus:define-json-model test-no-parser ((things not-parseable))) class-name TEST-NO-PARSER slots ((THINGS NOT-PARSEABLE)) ; Evaluation aborted on #<SIMPLE-ERROR "Could not find parser for ; class NOT-PARSEABLE. Please define a json model for it." ; {100599D903}>. #+end_src
** None, one or many semantics
Fields in class definitions are parsed as either nil (if missing from the json), a single instance if the field is not an array and isn't empty or a vector if the json contains an array of elements.
#+begin_src lisp CL-USER> (herodotus:define-json-model numbers (ns)) CL-USER> (ns (numbers-json:from-json "{ }")) NIL CL_USER> (ns (numbers-json:from-json "{ "ns": 1 }")) 1 CL-USER> (ns (numbers-json:from-json "{ "ns": [1, 2, 3] }")) #(1 2 3) #+end_src
** Setting the parsing case
The macro ~define-json-model~ has an optional third argument which specifies the case convention for parsing json fields. The options for this argument are
#+begin_src lisp :camel-case ;; camelCase :kebab-case ;; kebab-case :snake-case ;; snake_case :screaming-snake-case ;; SCREAMING_SNAKE_CASE #+end_src
The default value of the argument is ~:camel-case~. Below is an example of changing the default case to snake case.
#+begin_src lisp CL-USER> (herodotus:define-json-model snake-case (pet-snake) :snake-case) CL-USER> (pet-snake (snake-case-json:from-json "{ "pet_snake": "boa" }")) "boa" CL-USER> (herodotus:to-json (snake-case-json:from-json "{ "pet_snake": "boa" }")) "{"pet_snake":"boa"}" #+end_src
** Special case field names
Parsing specific field names can be done using the third argument of a field specifier. If a special field name is provided it doesn't have to match the name of the slot in the CLOS class and can use any formatting convention.
#+begin_src lisp CL-USER> (herodotus:define-json-model special-case ((unusual-format () "A_very-UniqueNAME")) CL-USER> (unusual-format (special-case-json:from-json "{ "A_very-UniqueNAME": "Phineas Fog" }")) "Phineas Fog" CL-USER> (herodotus:to-json (special-case-json:from-json "{ "A_very-UniqueNAME": "Phineas Fog" }")) "{"A_very-UniqueNAME":"Phineas Fog"}" #+end_src
** Macro specification
The ~define-json-model~ macro takes three arguments: ~name~, ~slots~ and an optional argument for ~case-type~. The ~name~ argument is the name of the generated CLOS class. The ~slots~ argument is a collection of slot descriptors and the ~case-type~ argument is a keyword.
Slot descriptors can be either symbols or lists. If a slot descriptor is a symbol then the value of the corresponding CLOS slot will be a deserialised json primitive in lisp form: a number, boolean, string, vector (for arrays), or hash-table (for objects).
If a slot descriptor is a list then first argument is the CLOS slot name, the second argument is either ~()~ or the name of a previously defined json model to deserialise the value of this field to. The optional third argument is a special case name for this field which can have custom formatting.
The ~case-type~ keyword argument is one of ~:screaming-snake-case~, ~:snake-case~, ~:kebab-case~ and ~:camel-case~ and defines the formatting convention for field names in a json object.
** Dependencies
The project depends on [[https://github.com/phmarek/yason][YASON]] which does the json parsing and serialisation under the hood, [[https://github.com/edicl/cl-ppcre][CL-PPCRE]] for text manipulation during code generation and [[https://github.com/kmx-io/alexandria][Alexandria]] for macro writing utilities.
** License
This project is provided under the MIT license. See the LICENSE file for details.
