Zulu-Inuoe / jzon

A correct and safe(er) JSON RFC 8259 reader/writer with sane defaults.
MIT License
151 stars 14 forks source link

Stringify replacer #17

Closed IAmRasputin closed 2 years ago

IAmRasputin commented 2 years ago

Adds a slot to the json-writer class specifying a function to apply to key-value pairs during stringification.

IAmRasputin commented 2 years ago
(defvar *json* (parse "{\"1\": \"one\", \"2\": \"two\"}"))
(stringify *json* :pretty t :stream *standard-output* :replacer (lambda (k v)
                                                                  (if (equal key "2")
                                                                      (string-upcase v)
                                                                      v)))

Should print:

{
    "1": "one",
    "2": "TWO"
}
IAmRasputin commented 2 years ago

see: https://github.com/Zulu-Inuoe/jzon/issues/16

IAmRasputin commented 2 years ago

With the most recent commit, the replacer function now returns:

T if the KV pair is to be included NIL if the KV pair is to be excluded completely (values t \<new value>) to change the value to stringify. For example:

(defvar *test-json* "{\"one\":[1,2,3],\"two\":{\"three\":\"four\"}}")
(defvar *parsed-json* (parse *test-json*))

(stringify *parsed-json*
           :stream *standard-output*
           :pretty t
           :replacer (lambda (k v)
                       (cond
                         ((equal k "three") nil)
                         ((arrayp v) (values t (subseq v 0 2)))
                         (t t)))

=> 

{
  "one": [
    1,
    2,
  ],
  "two": {}
}
Zulu-Inuoe commented 2 years ago

@IAmRasputin FYI I wrote up a handful of tests for this new replacer. Could you add these to jzon-tests.lisp ?

(test stringify-replacer-keeps-keys-on-t
  (is (string= "{\"x\":0}"
               (stringify (ph :x 0)
                          :replacer (lambda (k v)
                                      (declare (ignore k v))
                                      t)))))

(test stringify-replacer-filters-keys-on-nil
  (is (string= "{}"
               (stringify (ph :x 0)
                          :replacer (lambda (k v)
                                      (declare (ignore k v))
                                      nil)))))

(test stringify-replacer-filters-some-keys-on-nil
  (is (string= "{\"y\":0}"
               (stringify (ph :x 0 :y 0)
                          :replacer (lambda (k v)
                                      (declare (ignore v))
                                      (eq k :y))))))

(test stringify-replacer-replaces-values-using-multiple-values
  (is (string= "{\"x\":42}"
               (stringify (ph :x 0)
                          :replacer (lambda (k v)
                                      (declare (ignore k v))
                                      (values t 42))))))

(test stringify-replacer-ignores-second-value-on-nil
  (is (string= "{}"
               (stringify (ph :x 0)
                          :replacer (lambda (k v)
                                      (declare (ignore k v))
                                      (values nil 42))))))

(test stringify-replacer-is-called-on-sub-objects
  (is (string= "{\"x\":{\"a\":42}}"
               (stringify (ph :x (ph :a 0))
                          :replacer (lambda (k v)
                                      (declare (ignore v))
                                      (if (eq k :a)
                                          (values t 42)
                                          t))))))

(test stringify-replacer-is-called-recursively
  (is (string= "{\"x\":{\"y\":{\"z\":0}}}"
               (stringify (ph :x 0)
                          :replacer (lambda (k v)
                                      (declare (ignore v))
                                      (case k
                                        (:x (values t (ph :y 0)))
                                        (:y (values t(ph :z 0)))
                                        (t t)))))))
IAmRasputin commented 2 years ago

Added! And they pass on my machine, at least.

Zulu-Inuoe commented 2 years ago

Yeah I have the auto-running on CI. So the last 2 things I'm seeing missing here is

  1. replacer should be called (with key being nil) on the top level value
  2. replacer should be called on array entries, using the index of each entry as the key value. Here's two unit tests for those, but I also needed to update the existing tests.
(test stringify-replacer-keeps-keys-on-t
  (is (string= "{\"x\":0}"
               (stringify (ph :x 0)
                          :replacer (lambda (k v)
                                      (declare (ignore k v))
                                      t)))))

(test stringify-replacer-filters-keys-on-nil
  (is (string= "{}"
               (stringify (ph :x 0)
                          :replacer (lambda (k v)
                                      (declare (ignore v))
                                      (case k
                                        ((nil) t)
                                        (t nil)))))))

(test stringify-replacer-filters-some-keys-on-nil
  (is (string= "{\"y\":0}"
               (stringify (ph :x 0 :y 0)
                          :replacer (lambda (k v)
                                      (declare (ignore v))
                                      (case k
                                        ((nil) t)
                                        (t (eq k :y))))))))

(test stringify-replacer-replaces-values-using-multiple-values
  (is (string= "{\"x\":42}"
               (stringify (ph :x 0)
                          :replacer (lambda (k v)
                                      (declare (ignore v))
                                      (case k
                                        ((nil) t)
                                        (t (values t 42))))))))

(test stringify-replacer-ignores-second-value-on-nil
  (is (string= "{}"
               (stringify (ph :x 0)
                          :replacer (lambda (k v)
                                      (declare (ignore v))
                                      (case k
                                        ((nil) t)
                                        (t (values nil 42))))))))

(test stringify-replacer-is-called-on-sub-objects
  (is (string= "{\"x\":{\"a\":42}}"
               (stringify (ph :x (ph :a 0))
                          :replacer (lambda (k v)
                                      (declare (ignore v))
                                      (if (eq k :a)
                                          (values t 42)
                                          t))))))

(test stringify-replacer-is-called-recursively
  (is (string= "{\"x\":{\"y\":{\"z\":0}}}"
               (stringify (ph :x 0)
                          :replacer (lambda (k v)
                                      (declare (ignore v))
                                      (case k
                                        (:x (values t (ph :y 0)))
                                        (:y (values t(ph :z 0)))
                                        (t t)))))))

(test stringify-replacer-is-called-on-toplevel-value-with-nil-key
  (5am:is-true
   (let ((key-is-nil nil))
     (stringify 0 :replacer (lambda (k v)
                              (declare (ignore v))
                              (setf key-is-nil (null k))))
     key-is-nil))
  (5am:is-true
   (let ((value 0)
         (value-is-same nil))
     (stringify value :replacer (lambda (k v)
                                  (declare (ignore k))
                                  (setf value-is-same (eq value v) )))
     value-is-same)))

(test stringify-replacer-can-replace-toplevel-value
  (is (string= "42" (stringify 0 :replacer (lambda (k v)
                                             (declare (ignore k v))
                                             (values t 42))))))

(test stringify-replacer-is-called-on-array-elements-with-element-indexes
  (is (equal #(0 1 2)
             (let ((keys (list)))
               (stringify #(t t t) :replacer (lambda (k v)
                                               (declare (ignore v))
                                               (when k
                                                 (push k keys))))
               (nreverse keys)))))

By the way thank you so much for this!

IAmRasputin commented 2 years ago

This latest commit at least passes the tests. I also added test.sh for non-windows convenience.

Zulu-Inuoe commented 2 years ago

Nice, one last thing @IAmRasputin I think the code as written will end up calling replacer on every value recursively. I think you can fix it by doing (and (null context) %replacer) Inside of the write-value method

IAmRasputin commented 2 years ago

Like this?

Zulu-Inuoe commented 2 years ago

Yes, that does it I think!

here's a test for that case:

(test stringify-replacer-is-only-called-with-nil-on-toplevel-value
  (is (equalp '(#(1 2 3))
              (let ((called-on (list)))
                (stringify '#(1 2 3) :replacer (lambda (k v)
                                                 (when (null k)
                                                   (push v called-on))
                                                 t))
                (nreverse called-on)))))