;;; Copyright 2020 Google LLC
;;;
;;; Use of this source code is governed by an MIT-style
;;; license that can be found in the LICENSE file or at
;;; https://opensource.org/licenses/MIT.

(defpackage #:cl-protobufs.json
  (:use #:cl
        #:cl-protobufs
        #:cl-protobufs.implementation)
  ;; Shadow fmt from cl-protobufs text-format.
  (:shadow #:fmt)
  (:export #:print-json
           #:parse-json
           #:fmt)
  (:local-nicknames
   (#:pi #:cl-protobufs.implementation)
   (#:google #:cl-protobufs.google.protobuf)
   (#:wkt #:cl-protobufs.well-known-types)))

(in-package #:cl-protobufs.json)

;;; This file implements the protobuf JSON parser and printer.
;;; The exported symbols are parse-json and print-json.

(defun print-json (object &key (pretty-print-p t) (stream *standard-output*)
                          (camel-case-p t) (numeric-enums-p nil))
  "Prints a protocol buffer message to a stream in JSON format. The parameters
CAMEL-CASE-P and NUMERIC-ENUMS-P implement optional JSON printing options:
https://developers.google.com/protocol-buffers/docs/proto3#json_options.

Parameters:
  OBJECT: The protocol buffer message to print.
  PRETTY-PRINT-P: When true, generate line breaks and other human readable output
    in the json. When false, replace line breaks with spaces.
  STREAM: The stream to print to.
  CAMEL-CASE-P: If true print proto field names in camelCase.
  NUMERIC-ENUMS-P: If true, use enum numeric values rather than names."
  (print-json-impl object (when pretty-print-p 0) stream camel-case-p numeric-enums-p
                   nil))

(defun print-json-impl (object indent stream camel-case-p numeric-enums-p
                        spliced-p)
  "Prints a protocol buffer message to a stream in JSON format.
Parameters:
  OBJECT: The protocol buffer message to print.
  INDENT: Indent the output by INDENT spaces. If INDENT is NIL, then the
    output will not be pretty-printed.
  STREAM: The stream to print to.
  CAMEL-CASE-P: If true print proto field names in camelCase.
  NUMERIC-ENUMS-P: If true, use enum numeric values rather than names.
  SPLICED-P: Prints a protocol buffer object inside of the printing
    of another protocol buffer object as if they were spliced
    together. Currently only happens while printing a well-known-type.
    This happens because we have to print the well-known-type metadata.
    Example using Any well known type:
      {
        \"url\": \"type.googleapis.com/google.protobuf.Struct\",
        contained-proto
      }"
  (let* ((type (type-of object))
         (message (find-message-descriptor type :error-p t)))
    ;; If TYPE has a special JSON mapping, use that.
    (when (special-json-p type)
      (print-special-json object type stream indent camel-case-p numeric-enums-p)
      (return-from print-json-impl))
    (unless spliced-p
      (format stream "{")
      (when indent (format stream "~%")))
    ;; Boolean that tracks if a field is printed. Used for printing commas
    ;; correctly. If this object is spliced into an existing JSON object, then
    ;; a field has been already printed, so always print a comma.
    (let ((field-printed spliced-p))
      (dolist (field (proto-fields message))
        (when (if (eq (slot-value field 'pi::kind) :extends)
                  (has-extension object (slot-value field 'external-field-name))
                  (has-field object (slot-value field 'pi::external-field-name)))
          (let* ((name (if camel-case-p
                           (pi::proto-json-name field)
                           (proto-name field)))
                 (type (proto-class field))
                 (value
                   (if (eq (slot-value field 'pi::kind) :extends)
                       (get-extension object (slot-value field 'pi::external-field-name))
                       (proto-slot-value object (slot-value field 'pi::external-field-name)))))
            (if field-printed
                (format stream ",")
                (setf field-printed t))
            (if indent
                (format stream "~&~V,0T\"~A\": " (+ indent 2) name)
                (format stream "\"~A\":" name))
            (if (not (eq (proto-label field) :repeated))
                (print-field-to-json value type (and indent (+ indent 2))
                                     stream camel-case-p numeric-enums-p)
                (let (repeated-printed)
                  (format stream "[")
                  (pi::doseq (v value)
                    (if repeated-printed
                        (format stream ",")
                        (setf repeated-printed t))
                    (when indent (format stream "~&~V,0T" (+ indent 4)))
                    (print-field-to-json v type (and indent (+ indent 4))
                                         stream camel-case-p numeric-enums-p))
                  (if indent
                      (format stream "~&~V,0T]" (+ indent 2))
                      (format stream "]")))))))
      (dolist (oneof (pi::proto-oneofs message))
        (let* ((oneof-data (slot-value object (pi::oneof-descriptor-internal-name oneof)))
               (set-field (pi::oneof-set-field oneof-data)))
          (when set-field
            (let* ((field-desc (aref (pi::oneof-descriptor-fields oneof) set-field))
                   (type (proto-class field-desc))
                   (value (pi::oneof-value oneof-data))
                   (name (if camel-case-p
                             (pi::proto-json-name field-desc)
                             (proto-name field-desc))))
              (if field-printed
                  (format stream ",")
                  (setf field-printed t))
              (if indent
                  (format stream "~&~V,0T\"~A\": " (+ indent 2) name)
                  (format stream "\"~A\":" name))
              (print-field-to-json value type (and indent (+ indent 2))
                                   stream camel-case-p numeric-enums-p))))))
    (if indent
        (format stream "~&~V,0T}" indent)
        (format stream "}"))))

(defun print-field-to-json (value type indent stream camel-case-p numeric-enums-p)
  "Print a field to JSON format.

 Parameters:
  VALUE: The value held by the field
  TYPE: The proto-class slot of the field.
  INDENT: If non-nil, the amount to indent when pretty-printing.
  STREAM: The stream to print to.
  CAMEL-CASE-P: Passed recursively to PRINT-JSON.
  NUMERIC-ENUMS-P: Passed recursively to PRINT-ENUM-TO-JSON and PRINT-JSON."
  (let ((descriptor (or (find-message-descriptor type)
                        (find-enum-descriptor type)
                        (find-map-descriptor type))))
    (cond
      ((pi::scalarp type)
       (print-scalar-to-json value type stream))
      ((typep descriptor 'pi::message-descriptor)
       (print-json-impl value indent stream camel-case-p numeric-enums-p nil))
      ((typep descriptor 'pi::enum-descriptor)
       (print-enum-to-json value type stream numeric-enums-p))
      ((typep descriptor 'pi::map-descriptor)
       (print-map-to-json value descriptor indent
                          stream camel-case-p numeric-enums-p)))))

(defun print-scalar-to-json (value type stream)
  "Print scalar VALUE of type TYPE to STREAM."
  (ecase type
    ((int32 fixed32 uint32 sfixed32 sint32)
     (format stream "~D" value))
    ((int64 fixed64 uint64 sfixed64 sint64)
     (format stream "\"~D\"" value))
    ((float double-float)
     (format stream "~F" value))
    ((string)
     (format stream "\"~A\"" value))
    ((boolean)
     (format stream "~A" (if value "true" "false")))
    ((byte-vector)
     (format stream "\"~A\"" (cl-base64:usb8-array-to-base64-string value)))
    ((keyword)
     (format stream "\"~A\"" value))
    ((symbol)
     (let ((*package* (find-package "COMMON-LISP")))
       (format stream "\"~S\"" value)))))

(defun print-enum-to-json (value type stream numeric-enums-p)
  "Print an enum VALUE of type TYPE to STREAM. If NUMERIC-ENUMS-P, then print the enums value
rather than its name."
  (when (eql type 'google:null-value)
    (format stream "null")
    (return-from print-enum-to-json))
  (if numeric-enums-p
      (format stream "~D" (enum-keyword-to-int type value))
      (format stream "\"~A\"" (pi::enum-name->proto value))))

(defun print-map-to-json (value map-descriptor indent stream camel-case-p numeric-enums-p)
  "Print a map type to JSON.

Parameters:
  VALUE: The hash-table to print.
  MAP-DESCRIPTOR: The map-descriptor of the map.
  INDENT: If non-nil, the amount to indent when pretty-printing.
  STREAM: The stream to print to.
  CAMEL-CASE-P, NUMERIC-ENUMS-P: passed recursively to PRINT-FIELD-TO-JSON."
  (format stream "{")
  (when indent (format stream "~%"))
  (let ((pair-printed nil))
    (loop for k being the hash-key of value using (hash-value v)
          do (if pair-printed
                 (format stream ",")
                 (setf pair-printed t))
             (if indent
                 (format stream "~&~V,0T\"~A\": " (+ indent 2) k)
                 (format stream "\"~A\":"  (write-to-string k)))
             (print-field-to-json v (pi::proto-value-type map-descriptor)
                                  (and indent (+ indent 2)) stream camel-case-p numeric-enums-p)))
    (if indent
        (format stream "~&~V,0T}" indent)
        (format stream "}")))

;;; Parse objects that were serialized using JSON format.

;;; TODO(cgay): replace all assertions here with something that signals a
;;; subtype of protobuf-error and shows current stream position.

(defun parse-json (type
                   &key (stream *standard-input*) ignore-unknown-fields-p)
  "Parses JSON text into a protobuf messsage of type TYPE.

Parameters:
  TYPE: The object type as a symbol.
  STREAM: The stream to read from.
  IGNORE-UNKNOWN-FIELDS-P: If true, then skip fields which are not defined in the
    message TYPE descriptor. Otherwise, throw an error."
  (declare (type symbol type))
  (let ((message (find-message-descriptor type :error-p t)))
    (parse-json-impl message stream ignore-unknown-fields-p nil)))

(defun parse-json-impl (msg-desc stream ignore-unknown-fields-p spliced-p)
  "Parse a JSON formatted message with descriptor MSG-DESC from STREAM. If IGNORE-UNKNOWN-FIELDS-P
is true, then skip fields which are not defined in MSG-DESC. Otherwise, throw an error. If
SPLICED-P is true, then do not attempt to parse an opening bracket."
  (declare (type message-descriptor msg-desc))
  (let ((object (funcall (pi::get-constructor-name
                          (or (pi::proto-alias-for msg-desc)
                              (proto-class msg-desc)))))
        ;; Repeated slot names, tracks which slots need to be nreversed.
        (rslots ()))
    (when (special-json-p (proto-class msg-desc))
      (return-from parse-json-impl
        (parse-special-json (proto-class msg-desc)
                            stream
                            ignore-unknown-fields-p)))
    (unless spliced-p
      (pi::expect-char stream #\{))
    (loop
      (let* ((name  (pi::parse-string stream))
             (field (or (find-field-descriptor msg-desc name)
                        (find-field-descriptor-by-json-name msg-desc name)))
             (type  (and field (proto-class field)))
             (slot  (and field (pi::proto-external-field-name field))))
        (pi::expect-char stream #\:)
        (if (null field)
            ;; If FIELD is null, then we assume that MSG-DESC describes a
            ;; different version of the proto on the wire which doesn't
            ;; have FIELD, and continue,
            (if ignore-unknown-fields-p
                (skip-json-value stream)
                (error 'unknown-field-type
                       :format-control "unknown field ~S encountered in message ~S"
                       :format-arguments (list name msg-desc)))
            (let (val error-p null-p)
              (cond
                ((eql (peek-char nil stream nil) #\n)
                 (pi::expect-token-or-string stream "null")
                 (setf null-p t))
                ((eq (proto-label field) :repeated)
                 (pi::expect-char stream #\[)
                 (loop
                   (multiple-value-bind (data err)
                       (parse-value-from-json type :stream stream
                                                   :ignore-unknown-fields-p ignore-unknown-fields-p)
                     (if err
                         (setf error-p t)
                         (push data val)))
                   (if (eql (peek-char nil stream nil) #\,)
                       (pi::expect-char stream #\,)
                       (return)))
                 (pi::expect-char stream #\]))
                (t (multiple-value-setq (val error-p)
                     (parse-value-from-json type
                                            :stream stream
                                            :ignore-unknown-fields-p ignore-unknown-fields-p))))
              (cond
                (null-p nil)
                (error-p
                 (unknown-field-type type field msg-desc)
                 (return-from parse-json-impl))
                ((eq (pi::proto-kind field) :map)
                 (dolist (pair val)
                   (setf (gethash (car pair) (proto-slot-value object slot))
                         (cdr pair))))
                (t
                 (when slot
                   (setf (proto-slot-value object slot) val)
                   (when (eq (proto-label field) :repeated)
                     (pushnew slot rslots))))))))
      (if (eql (peek-char nil stream nil) #\,)
        (pi::expect-char stream #\,)
        (progn
          (pi::expect-char stream #\})
          (dolist (slot rslots)
            (setf (proto-slot-value object slot)
                  (nreverse (proto-slot-value object slot))))
          (return-from parse-json-impl object))))))

(defun parse-value-from-json (type &key (stream *standard-input*) ignore-unknown-fields-p)
  "Parse a single JSON value of type TYPE from STREAM. IGNORE-UNKNOWN-FIELDS-P is passed
   to recursive calls to PARSE-JSON-IMPL."
  (let ((desc (or (find-message-descriptor type)
                  (find-enum-descriptor type)
                  (find-map-descriptor type))))
    (cond ((pi::scalarp type)
           (case type
             ((float) (pi::parse-float stream))
             ((double-float) (pi::parse-double stream :append-d0 t))
             ((string) (pi::parse-string stream))
             ((boolean)
              (let ((token (pi::parse-token stream)))
                (cond ((string= token "true") t)
                      ((string= token "false") nil)
                      ;; Parsing failed, return T as a second
                      ;; value to indicate a failure.
                      (t (values nil t)))))
             ((byte-vector)
              (cl-base64:base64-string-to-usb8-array (pi::parse-string stream)))
             (otherwise
              (if (eql (peek-char nil stream nil) #\")
                  (let (ret)
                    (pi::expect-char stream #\")
                    (setf ret (pi::parse-signed-int stream))
                    (pi::expect-char stream #\")
                    ret)
                  (pi::parse-signed-int stream)))))
          ((typep desc 'pi::message-descriptor)
           (parse-json-impl desc stream ignore-unknown-fields-p nil))
          ((typep desc 'pi::enum-descriptor)
           (multiple-value-bind (name type-parsed)
               (pi::parse-token-or-string stream)
             ;; special handling for well known enum NullValue.
             (when (eql type 'google:null-value)
               (if (string= name "null")
                   (return-from parse-value-from-json :null-value)
                   (protobuf-error
                    "~S is not a valid keyword for well-known enum NullValue" name)))
             (let ((enum (if (eql type-parsed 'symbol)
                             ;; If the parsed type is a symbol, then the enum was printed
                             ;; as an integer. Otherwise, it is a string which names a
                             ;; keyword.
                             (find (parse-integer name) (pi::enum-descriptor-values desc)
                                   :key #'pi::enum-value-descriptor-value)
                             (find (pi::keywordify name)
                                   (pi::enum-descriptor-values desc)
                                   :key #'pi::enum-value-descriptor-name))))
               (and enum (pi::enum-value-descriptor-name enum)))))
          ;; In the case of maps, return a list of key-value pairs.
          ((typep desc 'pi::map-descriptor)
           (pi::expect-char stream #\{)
           (loop with pairs = ()
                 with key-type = (pi::proto-key-type desc)
                 with val-type = (pi::proto-value-type desc)
                 for pair = (cons nil nil)
                 do (setf (car pair)
                          (if (eql key-type 'string)
                              (pi::parse-string stream)
                              (parse-integer (pi::parse-string stream))))
                    (pi::expect-char stream #\:)
                    (setf (cdr pair) (parse-value-from-json val-type :stream stream))
                    (push pair pairs)
                    (if (eql (peek-char nil stream nil) #\,)
                        (pi::expect-char stream #\,)
                        (progn
                          (pi::expect-char stream #\})
                          (return pairs)))))
          (t (values nil t)))))

(defun skip-json-value (stream)
  "Skip a single JSON value in STREAM. This can
be either an array, object, or primitive."
  (pi::skip-whitespace stream)
  (case (peek-char nil stream nil)
    ((#\{) (skip-json-object stream))
    ((#\[) (skip-json-array stream))
    (t (pi::parse-token-or-string stream))))

(defun skip-json-array (stream)
  "Skip a JSON array in STREAM."
  (pi::expect-char stream #\[)
  (loop do (skip-json-value stream)
           (if (eql (peek-char nil stream nil) #\,)
               (pi::expect-char stream #\,)
               (return)))
  (pi::skip-whitespace stream)
  (pi::expect-char stream #\]))

(defun skip-json-object (stream)
  "Skip a JSON object in STREAM."
  (pi::expect-char stream #\{)
  (loop do (pi::parse-string stream)
           (pi::expect-char stream #\:)
           (skip-json-value stream)
           (if (eql (peek-char nil stream nil) #\,)
               (pi::expect-char stream #\,)
               (return)))
  (pi::skip-whitespace stream)
  (pi::expect-char stream #\}))

(defun find-field-descriptor-by-json-name (msg-desc name)
  "Return the field-descriptor with json-name NAME in MSG-DESC."
  (or (find name (proto-fields msg-desc) :key #'pi::proto-json-name :test #'string=)
      (loop for oneof in (pi::proto-oneofs msg-desc)
              thereis (find name (pi::oneof-descriptor-fields oneof)
                            :key #'pi::proto-json-name
                            :test #'string=))))

;; Special JSON mappings for well known types below

(defun special-json-p (type)
  "Check if the message TYPE has a special JSON mapping."
  (member type '(google:any
                 google:timestamp
                 google:duration
                 google:struct
                 google:value
                 google:field-mask
                 google:list-value
                 google:bool-value
                 google:string-value
                 google:bytes-value
                 google:double-value
                 google:float-value
                 google:int32-value
                 google:int64-value
                 google:u-int32-value
                 google:u-int64-value)))

(defun wrapper-message->type (type)
  "For a well known wrapper type TYPE, return the type being wrapped."
  (ecase type
    ((google:bool-value) 'boolean)
    ((google:string-value) 'string)
    ((google:bytes-value) 'byte-vector)
    ((google:double-value) 'double-float)
    ((google:float-value) 'float)
    ((google:int32-value) 'int32)
    ((google:int64-value) 'int64)
    ((google:u-int32-value) 'uint32)
    ((google:u-int64-value) 'uint64)))


(defun print-special-json (object type stream indent camel-case-p numeric-enums-p)
  "For an OBJECT whose TYPE is a well-known type, print the object's special JSON mapping
to STREAM. INDENT, CAMEL-CASE-P, and NUMERIC-ENUMS-P are passed recursively to
PRINT-JSON-IMPL for any types."
  (declare (type symbol type))
  (case type
    ((google:any)
     (let ((url (google:any.type-url object))
           (packed-message (wkt:unpack-any object)))
       (format stream "{")
       (if indent
           (format stream "~&~V,0T\"url\": \"~A\"" (+ indent 2) url)
           (format stream "\"url\": \"~A\"" url))
       (if (special-json-p (type-of packed-message))
           ;; special handling for nested special json mapping within an ANY.
           (progn
             (if indent
                 (format stream ",~&~V,0T\"value\": " (+ indent 2))
                 (format stream ",\"value\":"))
             (print-special-json packed-message (type-of packed-message) stream
                                 (and indent (+ indent 2)) camel-case-p numeric-enums-p)
             (if indent
                 (format stream "~&~V,0T}" indent)
                 (format stream "}")))
           (print-json-impl packed-message indent stream camel-case-p
                            numeric-enums-p t))))
    ((google:timestamp)
     #-os-windows
     (let* ((nsec (google:timestamp.nanos object))
            (timestamp (local-time:unix-to-timestamp
                        (google:timestamp.seconds object)
                        :nsec nsec))
            (prefix '((:year 4) #\- (:month 2) #\- (:day 2) #\T
                      (:hour 2) #\: (:min 2) #\: (:sec 2)))
            (suffix '(:gmt-offset-or-z))
            (format (cond ((= nsec 0) (append prefix suffix))
                          ((= (mod nsec 1000000) 0) (append prefix '(#\. (:msec 3)) suffix))
                          ((= (mod nsec 1000) 0) (append prefix '(#\. (:usec 6)) suffix))
                          (t (append prefix '(#\. (:nsec 9)) suffix)))))
       (format stream "~S" (local-time:format-timestring
                            nil timestamp
                            :format format
                            :timezone local-time:+utc-zone+))))
    ((google:duration)
     (let ((seconds (google:duration.seconds object))
           (nanos (google:duration.nanos object)))
       (assert (eql (signum seconds) (signum nanos)))
       (format stream "\"~D.~V,VDs\"" seconds 9 #\0 (abs nanos))))
    ((google:field-mask)
     (let ((paths (google:field-mask.paths object)))
       (format stream "\"~{~a~^,~}\"" (mapcar (lambda (name)
                                            (pi::camel-case-but-one name '(#\_)))
                                              paths))))
    ((google:struct)
     (let ((field (pi::%find-field-descriptor (find-message-descriptor type) 'google::%fields)))
       (print-map-to-json (google:fields object) (find-map-descriptor (proto-class field))
                          indent stream camel-case-p numeric-enums-p)))
    ((google:list-value)
     (format stream "[")
     (loop for print-comma-p = nil then t
           for value in (google:values object)
           do (when print-comma-p (format stream ","))
              (when indent (format stream "~&~V,0T" (+ 2 indent)))
              (print-field-to-json value 'google:value (and indent (+ indent 2))
                                   stream camel-case-p numeric-enums-p))
     (if indent
         (format stream "~&~V,0T]" indent)
         (format stream "]")))
    ((google:value)
     (let* ((oneof-data (slot-value object 'google::%kind))
            ;; The wkt Value consists of a single oneof, so the first oneof in the
            ;; descriptor's list is the one we are looking for.
            (oneof-desc (first (pi::proto-oneofs (find-message-descriptor type))))
            (set-field (pi::oneof-set-field oneof-data)))
       (assert set-field ()
               "Message ~S must have a set 'kind' oneof as it has well-known-type 'Value'." object)
       (let* ((field (aref (pi::oneof-descriptor-fields oneof-desc)
                           (pi::oneof-set-field oneof-data)))
              (value (pi::oneof-value oneof-data)))
         (print-field-to-json value (proto-class field)
                              indent stream camel-case-p numeric-enums-p))))
    ;; Otherwise, TYPE is a wrapper type.
    (t (if object
           (print-scalar-to-json (google:value object)
                                 (wrapper-message->type type)
                                 stream)
           (format stream "null")))))

(defun parse-special-json (type stream ignore-unknown-fields-p)
  "Parse a well known type TYPE from STREAM. IGNORE-UNKNOWN-FIELDS-P is passed to recursive
calls to PARSE-JSON-IMPL."
  ;; If the stream starts with 'n', then the data is NULL. In which case, return NIL.
  ;; In all cases except the `Value` well-known-type, we return NIL. However, if TYPE is
  ;; GOOGLE:VALUE, then we return the wrapper enum that represents null as per the spec.
  (when (eql (peek-char nil stream nil) #\n)
    (pi::expect-token-or-string stream "null")
    (return-from parse-special-json
      (and (eql type 'google:value)
           (google:make-value :null-value :null-value))))
  (case type
    ((google:any)
     (pi::expect-char stream #\{)
     (pi::expect-token-or-string stream "url")
     (pi::expect-char stream #\:)
     (let* ((type-url (pi::parse-string stream))
            (type (wkt::resolve-type-url type-url)))
       (pi::expect-char stream #\,)
       (if (not (special-json-p type))
           ;; Parse the remaining elements in the object into a new message, then pack that message.
           (wkt:pack-any
            (parse-json-impl (find-message-descriptor type :error-p t)
                             stream ignore-unknown-fields-p t))
           ;; If URL names a well-known-type, then the next element in the object has key "VALUE",
           ;; and the value is the special JSON format. Parse that and close the object.
           (let (ret)
             (pi::expect-token-or-string stream "value")
             (pi::expect-char stream #\:)
             (setf ret (parse-special-json type stream ignore-unknown-fields-p))
             (pi::expect-char stream #\})
             (wkt:pack-any ret)))))

    ((google:timestamp)
     #-os-windows
     (let* ((timestring (pi::parse-string stream))
            (timestamp (local-time:parse-rfc3339-timestring timestring)))
       (google:make-timestamp
        :seconds (local-time:timestamp-to-unix timestamp)
        :nanos (local-time:nsec-of timestamp))))

    ;; Durations can feasibly have 64-bit seconds place, so parsing a float/double is lossy.
    ((google:duration)
     (pi::expect-char stream #\")
     (let ((seconds (pi::parse-signed-int stream)))
       (ecase (peek-char nil stream nil)
         ;; Duration has no decimal component.
         ((#\s)
          (pi::expect-char stream #\s)
          (pi::expect-char stream #\")
          (google:make-duration :seconds seconds))
         ((#\.)
          (pi::expect-char stream #\.)
          ;; Parse the decimal part of the string, and convert to nanoseconds.
          (let ((remainder (pi::parse-token stream)))
            (assert (eql (char remainder (1- (length remainder))) #\s)
                    nil "Duration string ~S.~A does end with \"s\"" seconds remainder)
            (pi::expect-char stream #\")
            (let* ((decimals (subseq remainder 0 (1- (length remainder))))
                   ;; If there are more than 9 decimal points, trim to length 9.
                   (decimals (if (< 9 (length decimals))
                                 (subseq decimals 0 10)
                                 decimals))
                   (dec-length (length decimals)))
              (google:make-duration
               :seconds seconds
               ;; Nanoseconds are in the range 0 through 999,999,999. Pad the decimal string
               ;; with 0s to make the string have total length 9.
               ;; Lastly, multiply by the sign of SECONDS, as NANOS and and SECONDS must
               ;; have the same sign.
               :nanos (* (if (= 0 seconds) 1 (signum seconds))
                         (parse-integer (concatenate 'string
                                                     decimals
                                                     (make-string (- 9 dec-length)
                                                                  :initial-element #\0)))))))))))

    ;; Field masks are in the form \"camelCasePath1,path2,path3\". We need to first split,
    ;; then convert to proto field name format (lowercase, separated by underscore).
    ((google:field-mask)
     (let ((camel-case-paths (pi::split-string (pi::parse-string stream)
                                                       :separators '(#\,))))
       (google:make-field-mask
        :paths (mapcar (lambda (path) (nstring-downcase (pi::uncamel-case path #\_)))
                       camel-case-paths))))

    ((google:struct)
     (pi::expect-char stream #\{)
     (loop with ret = (google:make-struct)
           for key = (pi::parse-string stream)
           do (pi::expect-char stream #\:)
              (setf (google:struct.fields-gethash key ret)
                    (parse-special-json 'google:value stream ignore-unknown-fields-p))
              (if (eql (peek-char nil stream nil) #\,)
                  (pi::expect-char stream #\,)
                  (progn
                    (pi::expect-char stream #\})
                    (return ret)))))

    ((google:list-value)
     (pi::expect-char stream #\[)
     (loop with ret = (google:make-list-value)
           do (multiple-value-bind (data err)
                  (parse-value-from-json 'google:value
                                         :stream stream
                                         :ignore-unknown-fields-p ignore-unknown-fields-p)
                (if err
                    (error "Error while parsing well known type VALUE from JSON format.")
                    (push data (google:list-value.values ret))))
              (if (eql (peek-char nil stream nil) #\,)
                  (pi::expect-char stream #\,)
                  (progn
                    (pi::expect-char stream #\])
                    (return ret)))))

    ((google:value)
     (case (peek-char nil stream nil)
       ((#\{) (google:make-value
               :struct-value (parse-special-json 'google:struct stream ignore-unknown-fields-p)))
       ((#\[) (google:make-value
               :list-value (parse-special-json 'google:list-value stream ignore-unknown-fields-p)))
       ((#\") (google:make-value :string-value (pi::parse-string stream)))
       ((#\t)
        (pi::expect-token-or-string stream "true")
        (google:make-value :bool-value t))
       ((#\f)
        (pi::expect-token-or-string stream "false")
        (google:make-value :bool-value nil))
       ;; Otherwise, the value has type double.
       (t (google:make-value :number-value (pi::parse-double stream :append-d0 t)))))

    ;; Otherwise, the well known type is a wrapper type.
    (t (let ((object (funcall (pi::get-constructor-name type)))
             (value (parse-value-from-json (wrapper-message->type type) :stream stream)))
         (setf (google:value object) value)
         object))))

(defun fmt (stream proto colon-p at-sign-p &optional width &rest other-args)
  "Format command for protobufs
   ~/cl-protobufs.json:fmt/ emits a non-pretty-printed protobuf of PROTO to STREAM.
   ~@/cl-protobufs.json:fmt/ emits a pretty-printed protobuf of PROTO to STREAM.
   COLON-P and AT-SIGN-P are the usual for format directives.
   WIDTH and OTHER-ARGS are ignored."
  (declare (ignore width))
  (cond (other-args (error "FORMAT directive ~~/cl-protobufs.json:fmt/ takes only one argument."))
        (colon-p (error "FORMAT directive ~~/cl-protobufs.json:fmt/ does not take colons."))
        (t (print-json proto :stream stream :pretty-print-p at-sign-p))))
