src/mop.lisp @ 6d9143f80df3

Reuse string output streams
author Steve Losh <steve@stevelosh.com>
date Fri, 14 Aug 2020 23:06:06 -0400
parents e524dd8f7940
children 8e500ea0d9ff
(in-package :jarl)

;;;; Object Parsers -----------------------------------------------------------
(defun lisp-case-to-snake-case (string)
  "Convert a Lisp-cased string designator `\"FOO-BAR\"` into snake cased `\"foo_bar\"`."
  (substitute #\_ #\- (string-downcase string)))

(defclass json-class (standard-class)
  ((slot-name-to-json-name :accessor slot-name-to-json-name
                           :initarg :slot-name-to-json-name
                           :initform 'lisp-case-to-snake-case)
   (unknown-slots :accessor unknown-slots
                  :initarg :unknown-slots
                  :initform :discard)
   (name-initarg-map :accessor name-initarg-map)
   (slot-name-alist :accessor slot-name-alist)))

(defmethod c2mop:validate-superclass ((class json-class) (superclass standard-class))
  t)


(defclass json-direct-slot-definition (c2mop:standard-direct-slot-definition)
  ((json-class :initarg :json :accessor json-class)
   (json-name :initarg :json/name :accessor json-name)))

(defclass json-effective-slot-definition (c2mop:standard-effective-slot-definition)
  ((json-class :initarg :json :accessor json-class)
   (json-name :initarg :json/name :accessor json-name)
   (json-initarg :accessor json-initarg)))

(defmethod c2mop:direct-slot-definition-class ((class json-class) &rest initargs)
  (if (getf initargs :json)
    (find-class 'json-direct-slot-definition)
    (call-next-method)))

(defvar *effective-slot-definition-class* nil)

(defmethod c2mop:effective-slot-definition-class ((class json-class) &rest initargs)
  (declare (ignore initargs))
  ;; I'm not sure why we need to use this hack here, but for some reason
  ;; initargs doesn't contain the slot options like :json and :json/name here
  ;; like it does in direct-slot-definition-class.  So we need another way to
  ;; know which class to use here.
  (or *effective-slot-definition-class* (call-next-method)))

(defmethod c2mop:compute-effective-slot-definition ((class json-class) name direct-slots)
  (if (not (some (lambda (dslot)
                   (typep dslot 'json-direct-slot-definition))
                 direct-slots))
    (call-next-method)
    (let* ((*effective-slot-definition-class* (find-class 'json-effective-slot-definition))
           (eslot (call-next-method))
           (dslot (first direct-slots)) ; todo be smarter about coalescing these
           (initarg (gensym (format nil "json-initarg-~A" name)))) ; todo nicer name
      (setf (json-name eslot) (if (slot-boundp dslot 'json-name)
                                (json-name dslot)
                                (funcall (slot-name-to-json-name class) name)) ; todo make this less shitty
            (json-class eslot) (if (slot-boundp dslot 'json-class)
                                 (canonicalize-class-designator (json-class dslot))
                                 '(t))
            (json-initarg eslot) initarg) ; todo nicer name
      (push initarg (c2mop:slot-definition-initargs eslot))
      eslot)))


(defun json-slots (class)
  (remove-if-not (lambda (slot) (typep slot 'json-effective-slot-definition))
                 (c2mop:class-slots class)))

(defun make-name-initarg-map (class)
  "Return a name/initarg map for the JSON slots of `class`.

  The result will be a hash table of `{name: (initarg class contained-class)}`.

  "
  (let* ((slots (json-slots class))
         (result (make-hash-table :test #'equal :size (length slots))))
    (dolist (slot slots)
      (destructuring-bind (c &optional cc) (json-class slot)
        (setf (gethash (json-name slot) result)
              (list (json-initarg slot) c cc))))
    result))

(defun make-slot-name-alist (class)
  (mapcar (lambda (slot)
            (cons (c2mop:slot-definition-name slot)
                  (json-name slot)))
          (json-slots class)))

(defmethod shared-initialize ((class json-class) slot-names
                              &rest initargs
                              &key slot-name-to-json-name unknown-slots
                              &allow-other-keys)
  (apply #'call-next-method class slot-names
         (append
           (when slot-name-to-json-name ; todo assert length = 1
             (list :slot-name-to-json-name (first slot-name-to-json-name)))
           (when unknown-slots ; todo assert length = 1
             (list :unknown-slots (first unknown-slots)))
           initargs)))

(defmethod c2mop:finalize-inheritance :after ((class json-class))
  (setf (name-initarg-map class) (make-name-initarg-map class)
        (slot-name-alist class) (make-slot-name-alist class)))


;;;; Read ---------------------------------------------------------------------
(defun parse-json-class (class-name class input)
  (let ((ch (r input)))
    (unless (eql ch #\{)
      (e class-name input "expected ~S but got ~S" #\{ ch)))
  (incf-depth input)
  (skip-whitespace input)
  (if (eql (p input) #\})
    (progn (r input)
           (decf-depth input)
           (make-instance class))
    (loop
      :with unknown = (unknown-slots class)
      :with map = (name-initarg-map class)
      :with init = (list)
      :for name = (read% 'string nil input)
      :for sep = (parse-kv-separator class-name input)
      :for (initarg c cc) = (gethash name map)
      :do (progn
            (if (null initarg)
              (ecase unknown
                (:discard (read% t nil input))
                (:error (e class-name input "got unknown object attribute ~S" name)))
              (progn
                (push (read% c cc input) init)
                (push initarg init)))
            (skip-whitespace input)
            (let ((ch (r input)))
              (case ch
                (#\} (decf-depth input) (loop-finish))
                (#\, (skip-whitespace input))
                (t (e class-name input "expected ~S or ~S but got ~S." #\} #\, ch)))))
      :finally (return (apply #'make-instance class init)))))

(defmethod read% ((class-name symbol) (contained-class null) (input input))
  (let ((class (find-class class-name nil)))
    (typecase class
      (json-class
        (c2mop:ensure-finalized class)
        (parse-json-class class-name class input))
      (null (error "Cannot find class ~S to parse JSON into." class-name))
      (t (error "Cannot parse JSON into class ~S because that class is not a ~S"
                class-name 'json-class)))))


;;;; Printing -----------------------------------------------------------------
(defmethod print% (thing stream)
  (let ((class (class-of thing)))
    (if (typep class 'json-class)
      (progn
        (write-char #\{ stream)
        (loop :with first = t
              :for (slot . name) :in (slot-name-alist class)
              :when (slot-boundp thing slot)
              :do (progn (if first
                           (setf first nil)
                           (write-char #\, stream))
                         (print% name stream)
                         (write-char #\: stream)
                         (print% (slot-value thing slot) stream)))
        (write-char #\} stream))
      (error "Don't know how to print object ~S of class ~S as JSON." thing class))))