diff --git a/ciel.asd b/ciel.asd index 10acf36..5c8d00d 100644 --- a/ciel.asd +++ b/ciel.asd @@ -39,7 +39,8 @@ :closer-mop :cl-ansi-text :cl-csv - :shasht + :shasht ;; json + :cl-json-pointer :dissect :fset :generic-cl @@ -131,6 +132,7 @@ :components ((:module "src" :components ((:file "packages") + (:file "json-pointer-minus") (:file "ciel"))) (:module "src/more-docstrings" :components diff --git a/docs/README.md b/docs/README.md index 1afe582..5ba5a8c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,7 @@ It's Common Lisp, batteries included. It comes in 3 forms: -- a binary, to run CIEL **scripts**. +- a binary, to run CIEL **scripts**: fast start-up times, standalone binary, built-in utilities. - a simple full-featured **REPL** for the terminal. - a **Lisp library**. diff --git a/docs/index.html b/docs/index.html index 48cf707..5363c78 100644 --- a/docs/index.html +++ b/docs/index.html @@ -48,7 +48,7 @@ var footer = [ '
', '' ].join(''); diff --git a/docs/libraries.md b/docs/libraries.md index 635e475..ae27385 100644 --- a/docs/libraries.md +++ b/docs/libraries.md @@ -14,7 +14,7 @@ It's always (access my-structure :elt) ``` -for an alist, a hash-table, a struct, an object… Use `accesses` for nested access (specially useful with JSON). +for an alist, a hash-table, a struct, an object… Use `accesses` for nested access (specially useful with JSON). See also `json-pointer`. ### Hash-table utilities (Alexandria and Serapeum) @@ -161,9 +161,13 @@ See also: ### JSON -We use [shasht](https://github.com/yitzchak/shasht). It has a `json` nickname. +We use [shasht](https://github.com/yitzchak/shasht) to read and write JSON. It has a `json` nickname. -It is one of the newest and one of the best JSON handling libraries. +We use [cl-json-pointer](https://github.com/y2q-actionman/cl-json-pointer/) to refer to nested keys with a short syntax. It has a `json-pointer` nickname. + +#### read-json, write-json + +Shasht is one of the newest and one of the best JSON handling libraries. To encode an object to a stream (standard output, a string, or another stream), use `write-json`. Its signature is: @@ -342,6 +346,70 @@ There is also a keyword variant `write-json*` which will set the various dynamic variables from supplied keywords and will default to the current dynamic value of each keyword. +#### JSON pointer + +JSON pointers ([RFC 6901](https://www.rfc-editor.org/rfc/rfc6901)) are especially handy to manipulate nested objects. We can access, set and delete keys and values with short pointers, which are strings. + +Example: + +~~~lisp +(defparameter *json-data* + "{ + \"foo\": [\"bar\", \"baz\"] + }") + +(let ((obj (json:read-json *json-data*))) + (json-pointer:get-by obj "/foo/0")) +;; => "bar" +~~~ + +[cl-json-pointer](https://github.com/y2q-actionman/cl-json-pointer/) has some lengthy function names: + +- `get-by-json-pointer`, `set-by-json-pointer`… especially if we access them with the `json-pointer:` prefix. + +We provide shorten ones: + +- `get-by` `(obj pointer)` +- `set-by` `(obj pointer value)` +- `update-by` `(place pointer value)` +- `add-by` `(obj pointer value)` +- `delete-by` `(obj pointer)` +- `deletef-by` `(place pointer)` +- `remove-by` `(obj pointer)` +- `exists-p-by` `(obj pointer)` + +`get-by` traverses OBJ with POINTER and returns three values: + +- the found value (nil if not found), +- a generalized boolean saying the existence of the place pointed by POINTER, +- and NIL. + +JSON-POINTER functions actually take a dict (hash-table) as first argument. + +Examples: + +~~~lisp +(json-pointer:get-by (dict \"a\" + (dict \"aa\" 11)) + \"/a/aa\") +;; => 11 +~~~ + +Parse a JSON string with `shasht:read-json` before feeding the result to json-pointer: + +~~~lisp +(defvar *json-string* \"{\\\"foo\\\": [\\\"1\\\", \\\"2\\\"]}\") + +(let ((obj (shasht:read-json *json-string*))) + (json-pointer:get-by obj \"/foo\")) +;; => +#(\"1\" \"2\") +T +NIL +~~~ + +A JSON pointer starts with a "/". + ## Date and time diff --git a/src/json-pointer-minus.lisp b/src/json-pointer-minus.lisp new file mode 100644 index 0000000..b7d4c7a --- /dev/null +++ b/src/json-pointer-minus.lisp @@ -0,0 +1,99 @@ +;; +;; JSON-POINTER +;; +;; cl-json-pointer has lengthy functions: get-by-json-pointer, add-by-json-pointer, etc. +;; Let's create shorter ones: get-by etc. +;; +;; But why doesn't it accept a JSON string as input?! + +(in-package :ciel) + +(setf cl-json-pointer:*json-object-flavor* :shasht) + +(defun json-pointer-get-by (obj pointer &key (flavor cl-json-pointer:*json-object-flavor*)) + "Traverse OBJ with POINTER and return three values: + +- the found value (`nil' if not found), +- a generalized boolean saying the existence of the place pointed by POINTER, +- and NIL. + +GET-BY is a shorter name of cl-json-pointer:get-by-json-pointer added by CIEL. + +In CIEL, we use the SHASHT library to handle JSON. +SHASHT returns a hash-table. +JSON-POINTER functions take a dict (hash-table) as first argument. + +Examples: + +(json-pointer:get-by (dict \"a\" + (dict \"aa\" 11)) + \"/a/aa\") +;; => 11 + +Parse a JSON string with SHASHT:READ-JSON before feeding the result to json-pointer: + +(defvar *json-string* \"{\\\"foo\\\": [\\\"1\\\", \\\"2\\\"]}\") + +(let ((obj (shasht:read-json *json-string*))) + (json-pointer:get-by obj \"/foo\")) +;; => +#(\"1\" \"2\") +T +NIL +" + (cl-json-pointer:get-by-json-pointer obj pointer :flavor flavor)) + +#+(or) +(defvar *json-string* "{\"foo\": [\"1\", \"2\"]}") + +(defun json-pointer-set-by (obj pointer value &key (flavor cl-json-pointer:*json-object-flavor*)) + "Traverse OBJ with POINTER, set VALUE into the pointed place, and return the modified OBJ." + (cl-json-pointer:set-by-json-pointer obj pointer value :flavor flavor)) + +(defun json-pointer-update-by (place pointer value &key (flavor cl-json-pointer:*json-object-flavor*)) + "Set the result of SET-BY to the referred PLACE. + +UPDATE-BY is a modify macro for SET-BY (like PUSH or INCF)." + (cl-json-pointer:update-by-json-pointer place pointer value :flavor flavor)) + +(defun json-pointer-add-by (obj pointer value &key (flavor cl-json-pointer:*json-object-flavor*)) + "Works as `set-by', except this tries to make a new list when setting to lists." + (cl-json-pointer:add-by-json-pointer obj pointer value :flavor flavor)) + +(defun json-pointer-delete-by (obj pointer &key (flavor cl-json-pointer:*json-object-flavor*)) + "Traverse OBJ with POINTER, delete the pointed place, and return the modified OBJ." + (cl-json-pointer:delete-by-json-pointer obj pointer :flavor flavor)) + +(defun json-pointer-deletef-by (place pointer &key (flavor cl-json-pointer:*json-object-flavor*)) + "Set the result of DELETE-BY to the referred PLACE. + +This is a modify macro for DELETE-BY (like PUSH or INCF)." + (cl-json-pointer:deletef-by-json-pointer place pointer :flavor flavor)) + +(defun json-pointer-remove-by (obj pointer &key (flavor cl-json-pointer:*json-object-flavor*)) + "Like `delete-by', except this tries to make a new list when deleting from lists." + (cl-json-pointer:remove-by-json-pointer obj pointer :flavor flavor)) + +(defun json-pointer-exists-p-by (obj pointer &key (flavor cl-json-pointer:*json-object-flavor*)) + "Traverse OBJ with POINTER and return the existence of the place pointed by POINTER." + (cl-json-pointer:exists-p-by-json-pointer obj pointer :flavor flavor)) + +(defun json-pointer-shorten-functions () + "Shorten function names inside the cl-json-pointer package." + (let ((tuples (list + (list "GET-BY" #'json-pointer-get-by) + ;; NAME NEW FUNCTION + (list "ADD-BY" #'json-pointer-add-by) + (list "SET-BY" #'json-pointer-set-by) + (list "UPDATE-BY" #'json-pointer-update-by) + (list "DELETE-BY" #'json-pointer-delete-by) + (list "DELETEF-BY" #'json-pointer-deletef-by) + (list "REMOVE-BY" #'json-pointer-remove-by) + (list "EXISTS-P-BY" #'json-pointer-exists-p-by)))) + (loop for tuple in tuples + for sym = (intern (first tuple) 'cl-json-pointer) + do (export sym 'cl-json-pointer) + (setf (symbol-function sym) (second tuple)) + collect sym))) + +(json-pointer-shorten-functions) diff --git a/src/packages.lisp b/src/packages.lisp index 8cc08fc..e101a4d 100644 --- a/src/packages.lisp +++ b/src/packages.lisp @@ -20,6 +20,7 @@ (:csv :cl-csv) (:http :dexador) (:json :shasht) + (:json-pointer :cl-json-pointer) (:routes :easy-routes)))