fix cross-package and forward references in cl-protobufs
authorBen Wagner <benjaminwagner@google.com>
Tue, 20 Nov 2012 18:43:00 +0000 (13:43 -0500)
committerAlejandro R Sedeño <asedeno@google.com>
Tue, 27 Nov 2012 16:30:02 +0000 (11:30 -0500)
* Previously, if a field in a .proto file referenced a message in
  another proto file using a different lisp package, the cl-protobufs
  library would silently fail to serialize the field.  A similar
  problem would occur if a message defined later in the file used the
  lisp_name option to override the name generated by cl-protobufs.
  This change fixes these issues and others.
* Add conditions that are signaled when encountering an undefined
  type.
* Delay assigning lisp classes/types to fields and methods until all
  possible forward references have been parsed.
   * This allows the class slot to be unbound, so check for that case
     in print-object methods.
   * Add a test for forward references to messages that override the
     lisp name.
   * Add a test for references to messages and enums defined in
     another proto file with a different lisp package.
   * Change color-wheel-stability test, because it used "string" as
     the input type for an rpc, which seems to be disallowed (although
     I haven't found this documented anywhere).
* Signal errors during parsing for undefined types.
   * Add a test for these errors.  Add assert-error macro to qtest.
* Signal a condition if we are unable to find the definition for a
  field's type during serialization, deserialization, determining an
  object's serialized size, printing text format, parsing text format,
  or generating code for one of the above.
* Remove logic in find-qualified-name that indirects through lisp
  packages.  Proto packages and lisp packages do not necessarily map
  1-to-1.
* Always use the schema's lisp package for any symbols generated when
  parsing proto files.
* When generating lisp code using write-schema-as, set the package to
  the package used in the generated file, so that ~s will print the
  package prefix in the correct circumstances.
* Remove broken proto1 "streams" parsing ("returns" comes before
  "streams" in every example I've found); replace with proto2 syntax.
* In process-imports, the call to find-schema using a pathname was not
  giving the expected result.  Sidestep this issue by using the same
  logic to find the schema as is used earlier in the function.

17 files changed:
asdf-support.lisp
cl-protobufs.asd
conditions.lisp [new file with mode: 0644]
model-classes.lisp
parser.lisp
pkgdcl.lisp
printer.lisp
serialize.lisp
tests/cl-protobufs-tests.asd
tests/forward_reference.proto [new file with mode: 0644]
tests/lisp-reference-tests.lisp [new file with mode: 0644]
tests/package_test1.proto [new file with mode: 0644]
tests/package_test2.proto [new file with mode: 0644]
tests/pkgdcl.lisp
tests/qtest.lisp
tests/stability-tests.lisp
text-format.lisp

index a7a032c..71742c8 100644 (file)
                        (let ((*compile-file-pathname* nil)
                              (*load-pathname* fasl-file))
                          (load fasl-file)))))
-              (let* ((imported (find-schema base-path)))
+              (let* ((imported (find-schema (class-name->proto import-name))))
                 (when imported
                   (setf (proto-imported-schemas schema)
                         (nconc (proto-imported-schemas schema) (list imported))))
index 5a13e4d..97b846b 100644 (file)
@@ -33,7 +33,8 @@
               :depends-on ("packages")
               :components
                 ((:file "utilities")
-                 (:file "model-classes")))
+                 (:file "model-classes")
+                 (:file "conditions")))
      (:module "parsing"
               :serial t
               :pathname #p""
diff --git a/conditions.lisp b/conditions.lisp
new file mode 100644 (file)
index 0000000..3632d97
--- /dev/null
@@ -0,0 +1,74 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;                                                                  ;;;
+;;; Free Software published under an MIT-like license. See LICENSE   ;;;
+;;;                                                                  ;;;
+;;; Copyright (c) 2012 Google, Inc.  All rights reserved.            ;;;
+;;;                                                                  ;;;
+;;; Original author: Ben Wagner                                      ;;;
+;;;                                                                  ;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(in-package "PROTO-IMPL")
+
+;;; Protocol buffers conditions
+
+(define-condition undefined-type (simple-error)
+  ((type-name :type string
+              :reader error-type-name
+              :initarg :type-name
+              :documentation "The name of the type which can not be found."))
+  (:documentation "Indicates that a schema references a type which has not been defined.")
+  (:default-initargs :format-control "Undefined type:")
+  (:report (lambda (condition stream)
+             (format stream "~? ~s"
+                     (simple-condition-format-control condition)
+                     (simple-condition-format-arguments condition)
+                     (error-type-name condition)))))
+
+(define-condition undefined-field-type (undefined-type)
+  ((field :type protobuf-field
+          :reader error-field
+          :initarg :field
+          :documentation "The field whose type is TYPE-NAME."))
+  (:documentation "Indicates that a schema contains a message with a field whose type is not a
+                   primitive type and is not a known message (or extend) or enum.")
+  (:report (lambda (condition stream)
+             (format stream "~? Field ~s in message ~s has unknown type ~s."
+                     (simple-condition-format-control condition)
+                     (simple-condition-format-arguments condition)
+                     (error-field condition)
+                     (proto-parent (error-field condition))
+                     (error-type-name condition)))))
+
+(define-condition undefined-method-type (undefined-type)
+  ((method :type protobuf-method
+           :reader error-method
+           :initarg :method
+           :documentation "The method that references TYPE-NAME.")
+   (where :type string
+          :reader error-where
+          :initarg :where
+          :documentation "Description of which type referenced by the method is undefined."))
+  (:documentation "Superclass for `undefined-type' errors related to a `protobuf-method'.  Indicates
+                   that a schema contains a service with a method whose input, output, or stream
+                   type is not a known message (or extend).")
+  (:report (lambda (condition stream)
+             (format stream "~? ~a type for rpc ~s in service ~s has unknown type ~s."
+                     (simple-condition-format-control condition)
+                     (simple-condition-format-arguments condition)
+                     (error-where condition)
+                     (error-method condition)
+                     (proto-parent (error-method condition))
+                     (error-type-name condition)))))
+
+(define-condition undefined-input-type (undefined-method-type)
+  ()
+  (:default-initargs :where "Input"))
+
+(define-condition undefined-output-type (undefined-method-type)
+  ()
+  (:default-initargs :where "Output"))
+
+(define-condition undefined-stream-type (undefined-method-type)
+  ()
+  (:default-initargs :where "Stream"))
index a101773..9a7a130 100644 (file)
 
 (defun find-qualified-name (name protos
                             &key (proto-key #'proto-name) (full-key #'proto-qualified-name)
-                                 (lisp-key #'proto-class)
                                  relative-to)
   "Find something by its string name, first doing a simple name match,
    and, if that fails, exhaustively searching qualified names."
   (declare (ignore relative-to))
   (or (find name protos :key proto-key :test #'string=)
       ;;--- This needs more sophisticated search, e.g., relative to current namespace
-      (find name protos :key full-key  :test #'string=)
-      ;; Maybe we can find the symbol in Lisp land?
-      (multiple-value-bind (name package path other)
-          (proto->class-name name)
-        (declare (ignore path))
-        (let* ((name   (string name))
-               (symbol (or (and package (find-symbol name package))
-                           (and other
-                                (find-proto-package other)
-                                (find-symbol name (find-proto-package other))))))
-          (when symbol
-            (find symbol protos :key lisp-key))))))
+      (find name protos :key full-key  :test #'string=)))
 
 
 ;; A Protobufs schema, corresponds to one .proto file
 (defmethod print-object ((s protobuf-schema) stream)
   (print-unreadable-object (s stream :type t :identity t)
     (format stream "~@[~S~]~@[ (package ~A)~]"
-            (proto-class s) (proto-package s))))
+            (when (slot-boundp s 'class) (proto-class s)) (proto-package s))))
 
 (defgeneric make-qualified-name (proto name)
   (:documentation
 (defmethod print-object ((e protobuf-enum) stream)
   (print-unreadable-object (e stream :type t :identity t)
     (format stream "~S~@[ (alias for ~S)~]"
-            (proto-class e) (proto-alias-for e))))
+            (when (slot-boundp e 'class) (proto-class e)) (proto-alias-for e))))
 
 (defmethod make-qualified-name ((enum protobuf-enum) name)
   ;; The qualified name is the enum name "dot" the name
 (defmethod print-object ((m protobuf-message) stream)
   (print-unreadable-object (m stream :type t :identity t)
     (format stream "~S~@[ (alias for ~S)~]~@[ (group~*)~]~@[ (extended~*)~]"
-            (proto-class m) (proto-alias-for m)
+            (when (slot-boundp m 'class) (proto-class m))
+            (proto-alias-for m)
             (eq (proto-message-type m) :group)
             (eq (proto-message-type m) :extends))))
 
   (find name (proto-fields message) :key #'proto-value))
 
 (defmethod find-field ((message protobuf-message) (name string) &optional relative-to)
-  (find-qualified-name name (proto-fields message) :lisp-key #'proto-value
+  (find-qualified-name name (proto-fields message)
                        :relative-to (or relative-to message)))
 
 (defmethod find-field ((message protobuf-message) (index integer) &optional relative-to)
 (defmethod print-object ((f protobuf-field) stream)
   (print-unreadable-object (f stream :type t :identity t)
     (format stream "~S :: ~S = ~D~@[ (group~*)~]~@[ (extended~*)~]"
-            (proto-value f) (proto-class f) (proto-index f)
+            (proto-value f)
+            (when (slot-boundp f 'class) (proto-class f))
+            (proto-index f)
             (eq (proto-message-type f) :group)
             (eq (proto-message-type f) :extends))))
 
 (defmethod print-object ((m protobuf-method) stream)
   (print-unreadable-object (m stream :type t :identity t)
     (format stream "~S (~S) => (~S)"
-            (proto-class m) (proto-input-type m) (proto-output-type m))))
+            (proto-class m)
+            (when (slot-boundp m 'itype) (proto-input-type m))
+            (when (slot-boundp m 'otype) (proto-output-type m)))))
 
 
 ;;; Lisp-only extensions
index 5f77b75..712418d 100644 (file)
        (%make-source-location :pathname *protobuf-pathname*
                               :start-pos start :end-pos end)))
 
+(defgeneric resolve-lisp-names (protobuf)
+  (:documentation "Second pass of schema parsing which recursively resolves protobuf type names to
+                   lisp type names in all messages and services contained within 'protobuf'.  No
+                   return value."))
+
 ;; The syntax for Protocol Buffers is so simple that it doesn't seem worth
 ;; writing a sophisticated parser
 ;; Note that we don't put the result into *all-schemas*; that's done in 'define-schema'
       (let ((char (peek-char nil stream nil)))
         (cond ((null char)
                (remove-options schema "lisp_package")
+               (resolve-lisp-names schema)
                (return-from parse-schema-from-stream schema))
               ((proto-token-char-p char)
                (let ((token (parse-token stream)))
                                (name   (and option (proto-name option)))
                                (value  (and option (proto-value option))))
                           (when (and option (option-name= name "lisp_package"))
-                            (let ((package (or (find-proto-package value)
-                                               ;; Try to put symbols into the right package
-                                               (make-package (string-upcase value) :use ())
-                                               *protobuf-package*)))
-                              (setf (proto-lisp-package schema) value)
-                              (setq *protobuf-package* package)))))
+                            (set-lisp-package schema value))))
                        ((string= token "enum")
                         (parse-proto-enum stream schema))
                        ((string= token "extend")
               (t
                (error "Syntax error at position ~D" (file-position stream))))))))
 
+(defun set-lisp-package (schema lisp-package-name)
+  "Set the package for generated lisp names of 'schema'."
+  (check-type schema protobuf-schema)
+  (check-type lisp-package-name string)
+  (let ((package (or (find-proto-package lisp-package-name)
+                     ;; Try to put symbols into the right package
+                     (make-package (string-upcase lisp-package-name) :use ())
+                     *protobuf-package*)))
+    (setf (proto-lisp-package schema) lisp-package-name)
+    (setq *protobuf-package* package)))
+
+(defmethod resolve-lisp-names ((schema protobuf-schema))
+  "Recursively resolves protobuf type names to lisp type names in the messages and services in
+   'schema'."
+  (map () #'resolve-lisp-names (proto-messages schema))
+  (map () #'resolve-lisp-names (proto-services schema)))
+
 (defun parse-proto-syntax (stream schema &optional (terminator #\;))
   "Parse a Protobufs syntax line from 'stream'.
    Updates the 'protobuf-schema' object to use the syntax."
                        (substitute #\- #\_ package))))
     (setf (proto-package schema) package)
     (unless (proto-lisp-package schema)
-      (setf (proto-lisp-package schema) lisp-pkg))
-    (let ((package (or (find-proto-package lisp-pkg) *protobuf-package*)))
-      (setq *protobuf-package* package))))
+      (set-lisp-package schema lisp-pkg))))
 
 (defun parse-proto-import (stream schema &optional (terminator #\;))
   "Parse a Protobufs import line from 'stream'.
                              ((or (digit-char-p ch) (member ch '(#\- #\+ #\.)))
                               (parse-number stream))
                              ((eql ch #\{)
+                              ;;---bwagner: this is incorrect -- we need to find the field name in
+                              ;;   the locally-extended version of
+                              ;;   google.protobuf.[File,Message,Field,Enum,EnumValue,Service,Method]Options
+                              ;;   and get its type
                               (let ((message (find-message (or protobuf *protobuf*) key)))
                                 (if message
                                   ;; We've got a complex message as a value to an option
-                                  ;; This only shows up in custom optionss
+                                  ;; This only shows up in custom options
                                   (parse-text-format message :stream stream :parse-name nil)
                                   ;; Who knows what to do? Skip the value
                                   (skip-field stream))))
                (error "Unrecognized token ~A at position ~D"
                       token (file-position stream))))))))
 
+(defmethod resolve-lisp-names ((message protobuf-message))
+  "Recursively resolves protobuf type names to lisp type names in nested messages and fields of
+   'message'."
+  (map () #'resolve-lisp-names (proto-messages message))
+  (map () #'resolve-lisp-names (proto-fields message)))
+
 (defun parse-proto-extend (stream protobuf)
   "Parse a Protobufs 'extend' from 'stream'.
    Updates the 'protobuf-schema' or 'protobuf-message' object to have the message."
          (name (prog1 (parse-token stream)
                  (expect-char stream #\{ () "extend")
                  (maybe-skip-comments stream)))
+         ;;---bwagner: is 'extend' allowed to use a forward reference to a message?
          (message (find-message protobuf name))
          (extends (and message
                        (make-instance 'protobuf-message
                      (expect-char stream #\; () "message")
                      (maybe-skip-comments stream)))
              (packed (find-option opts "packed"))
-             (ptype  (if (member type '("int32" "int64" "uint32" "uint64" "sint32" "sint64"
-                                        "fixed32" "fixed64" "sfixed32" "sfixed64"
-                                        "string" "bytes" "bool" "float" "double") :test #'string=)
-                       (kintern type)
-                       type))
-             (class  (if (keywordp ptype) ptype (proto->class-name type *protobuf-package*)))
              (slot   (proto->slot-name name *protobuf-package*))
              (reqd   (kintern required))
              (field  (make-instance 'protobuf-field
                        :name  name
                        :type  type
-                       :class class
                        :qualified-name (make-qualified-name message name)
                        :parent message
                        ;; One of :required, :optional or :repeated
         (setf (proto-fields message) (nconc (proto-fields message) (list field)))
         field))))
 
+(defmethod resolve-lisp-names ((field protobuf-field))
+  "Resolves the field's protobuf type to a lisp type and sets `proto-class' for 'field'."
+  (let* ((type  (proto-type field))
+         (ptype (when (member type '("int32" "int64" "uint32" "uint64" "sint32" "sint64"
+                                     "fixed32" "fixed64" "sfixed32" "sfixed64"
+                                     "string" "bytes" "bool" "float" "double") :test #'string=)
+                  (kintern type)))
+         (message (unless ptype
+                    (or (find-message (proto-parent field) type)
+                        (find-enum (proto-parent field) type)))))
+    (unless (or ptype message)
+      (error 'undefined-field-type
+             :type-name type
+             :field field))
+    (setf (proto-class field) (or ptype (proto-class message))))
+  nil)
+
 (defun parse-proto-group (stream message required &optional extended-from)
   "Parse a (deprecated) Protobufs group from 'stream'.
    Updates the 'protobuf-message' object to have the group type and field."
          (name (slot-name->proto (proto->slot-name type)))
          (idx  (parse-unsigned-int stream))
          (msg  (parse-proto-message stream message type))
-         (class (proto->class-name type *protobuf-package*))
          (slot  (proto->slot-name name *protobuf-package*))
          (field (make-instance 'protobuf-field
                   :name  name
                   :type  type
-                  :class class
                   :qualified-name (make-qualified-name message name)
                   :parent message
                   :required (kintern required)
                (error "Unrecognized token ~A at position ~D"
                       token (file-position stream))))))))
 
+(defmethod resolve-lisp-names ((service protobuf-service))
+  "Recursively resolves protobuf type names to lisp type names for all methods of 'service'."
+  (map () #'resolve-lisp-names (proto-methods service)))
+
 (defun parse-proto-method (stream service index)
   "Parse a Protobufs method from 'stream'.
    Updates the 'protobuf-service' object to have the method."
          (out  (prog2 (expect-char stream #\( () "service")
                    (parse-token stream)
                  (expect-char stream #\) () "service")))
-         (strm (parse-token stream))            ;might be "streams"
-         (strm (and strm (string= strm "streams")
-                    (prog2 (expect-char stream #\( () "service")
-                        (parse-token stream)
-                      (expect-char stream #\) () "service"))))
          (opts (let ((opts (parse-proto-method-options stream)))
                  (when (or (null opts) (eql (peek-char nil stream nil) #\;))
                    (expect-char stream #\; () "service"))
                    :name  name
                    :qualified-name (make-qualified-name *protobuf* name)
                    :parent service
-                   :input-type  (proto->class-name in *protobuf-package*)
                    :input-name  in
-                   :output-type (proto->class-name out *protobuf-package*)
                    :output-name out
-                   :streams-type (and strm (proto->class-name strm *protobuf-package*))
-                   :streams-name strm
                    :index index
                    :options opts
                    :source-location (make-source-location stream loc (i+ loc (length name))))))
       (setf (proto-class method) stub
             (proto-client-stub method) stub
             (proto-server-stub method) (intern (format nil "~A-~A" 'do stub) *protobuf-package*)))
+    (let ((strm (find-option method "stream_type")))
+      (when strm
+        (setf (proto-streams-name method) strm)))
     (setf (proto-methods service) (nconc (proto-methods service) (list method)))
     method))
 
+(defmethod resolve-lisp-names ((method protobuf-method))
+  "Resolves input, output, and streams protobuf type names to lisp type names and sets
+   `proto-input-type', `proto-output-type', and, if `proto-streams-name' is set,
+   `proto-streams-type' on 'method'."
+  (let* ((input-name (proto-input-name method))
+         (output-name (proto-output-name method))
+         (streams-name (proto-streams-name method))
+         (service (proto-parent method))
+         (schema (proto-parent service))
+         (input-message (find-message schema input-name))
+         (output-message (find-message schema output-name))
+         (streams-message (and streams-name
+                               ;; this is supposed to be the fully-qualified name, but we don't
+                               ;; require that
+                               (find-message schema streams-name))))
+    (unless input-message
+      (error 'undefined-input-type
+             :type-name input-name
+             :method method))
+    (unless output-message
+      (error 'undefined-output-type
+             :type-name output-name
+             :method method))
+    (setf (proto-input-type method) (proto-class input-message))
+    (setf (proto-output-type method) (proto-class output-message))
+    (when streams-name
+      (unless streams-message
+        (error 'undefined-stream-type
+               :type-name streams-name
+               :method method))
+      (setf (proto-streams-type method) (proto-class streams-message))))
+  nil)
+
 (defun parse-proto-method-options (stream)
   "Parse any options in a Protobufs method from 'stream'.
    Returns a list of 'protobuf-option' objects."
index 3d977db..da80381 100644 (file)
    "PROTOBUF-METHOD"
    "PROTOBUF-TYPE-ALIAS"        ;Lisp-only extension
 
+   ;; Conditions
+   "UNDEFINED-FIELD-TYPE"
+   "UNDEFINED-INPUT-TYPE"
+   "UNDEFINED-OUTPUT-TYPE"
+   "UNDEFINED-STREAM-TYPE"
+   "ERROR-TYPE-NAME"
+   "ERROR-FIELD"
+   "ERROR-METHOD"
+
    ;; Object lookup
    "FIND-MESSAGE"
    "FIND-MESSAGE-FOR-CLASS"
index 9dcd2c3..40b01bf 100644 (file)
            (*show-lisp-enum-indexes*  show-enum-indexes)
            (*show-lisp-field-indexes* show-field-indexes)
            (*use-common-lisp-package* use-common-lisp)
-           (*protobuf-package* (or (find-proto-package lisp-pkg) *package*))
-           (*package* *protobuf-package*))
+           (*protobuf-package* (find-proto-package lisp-pkg))
+           ;; If *protobuf-package* has not been defined, print symbols
+           ;; from :common-lisp if *use-common-lisp-package* is true; or
+           ;; :keyword otherwise.  This ensures that all symbols will be
+           ;; read back correctly.
+           ;; (The :keyword package does not use any other packages, so
+           ;; all symbols will be printed with package prefixes.
+           ;; Keywords are always printed as :keyword.)
+           (*package* (or *protobuf-package*
+                          (when *use-common-lisp-package* (find-package :common-lisp))
+                          (find-package :keyword))))
       (when (or lisp-pkg pkg)
         (let ((pkg (string-upcase (or lisp-pkg pkg))))
           (format stream "~&(cl:eval-when (:execute :compile-toplevel :load-toplevel) ~
index e324cbf..981b87e 100644 (file)
                                           (tag  (make-tag type (proto-index field))))
                                      (doseq (v (read-slot object slot reader))
                                        (let ((v (funcall (proto-serializer msg) v)))
-                                         (setq index (serialize-prim v type tag buffer index))))))))
+                                         (setq index (serialize-prim v type tag buffer index))))))
+                                  (t
+                                   (error 'undefined-field-type
+                                          :format-control "While serializing ~s to protobuf,"
+                                          :format-arguments (list object)
+                                          :type-name (prin1-to-string type)
+                                          :field field))))
                            (t
                             (cond ((eq type :bool)
                                    ;; We have to handle optional boolean fields specially
                                        (let* ((v    (funcall (proto-serializer msg) v))
                                               (type (proto-proto-type msg))
                                               (tag  (make-tag type (proto-index field))))
-                                         (setq index (serialize-prim v type tag buffer index)))))))))))))
+                                         (setq index (serialize-prim v type tag buffer index))))))
+                                  (t
+                                   (error 'undefined-field-type
+                                          :format-control "While serializing ~s to protobuf,"
+                                          :format-arguments (list object)
+                                          :type-name (prin1-to-string type)
+                                          :field field)))))))))
         (declare (dynamic-extent #'do-field))
         (dolist (field (proto-fields message))
           (do-field object message field))))
                                           (tag  (make-tag type (proto-index field))))
                                      (doseq (v (read-slot object slot reader))
                                        (let ((v (funcall (proto-serializer msg) v)))
-                                         (iincf size (prim-size v type tag))))))))
+                                         (iincf size (prim-size v type tag))))))
+                                  (t
+                                   (error 'undefined-field-type
+                                          :format-control "While computing the size of ~s in bytes,"
+                                          :format-arguments (list object)
+                                          :type-name (prin1-to-string type)
+                                          :field field))))
                            (t
                             (cond ((eq type :bool)
                                    (let ((v (cond ((or (eq (proto-required field) :required)
                                        (let* ((v    (funcall (proto-serializer msg) v))
                                               (type (proto-proto-type msg))
                                               (tag  (make-tag type (proto-index field))))
-                                         (iincf size (prim-size v type tag)))))))))))))
+                                         (iincf size (prim-size v type tag))))))
+                                  (t
+                                   (error 'undefined-field-type
+                                          :format-control "While computing the size of ~s in bytes,"
+                                          :format-arguments (list object)
+                                          :type-name (prin1-to-string type)
+                                          :field field)))))))))
         (declare (dynamic-extent #'do-field))
         (dolist (field (proto-fields message))
           (do-field object message field))
                                     (tag   (make-tag class (proto-index field))))
                                `(,iterator (,vval ,reader)
                                   (let ((,vval (funcall #',(proto-serializer msg) ,vval)))
-                                    (setq ,vidx (serialize-prim ,vval ,class ,tag ,vbuf ,vidx))))))))))
+                                    (setq ,vidx (serialize-prim ,vval ,class ,tag ,vbuf ,vidx)))))))
+                           (t
+                            (error 'undefined-field-type
+                                   :format-control "While generating the serialize-object method ~
+                                                    for ~s,"
+                                   :format-arguments (list message)
+                                   :type-name (prin1-to-string class)
+                                   :field field)))))
                   (t
                    (cond ((keywordp class)
                           (collect-serializer
                              `(let ((,vval ,reader))
                                 (when ,vval
                                   (let ((,vval (funcall #',(proto-serializer msg) ,vval)))
-                                    (setq ,vidx (serialize-prim ,vval ,class ,tag ,vbuf ,vidx))))))))))))))
+                                    (setq ,vidx (serialize-prim ,vval ,class ,tag ,vbuf ,vidx))))))))
+                         (t
+                          (error 'undefined-field-type
+                                 :format-control "While generating the serialize-object method ~
+                                                  for ~s,"
+                                 :format-arguments (list message)
+                                 :type-name (prin1-to-string class)
+                                 :field field))))))))
       `(defmethod serialize-object
            (,vobj (,vclass (eql ,message)) ,vbuf &optional (,vidx 0) visited)
          (declare #.$optimize-serialization)
                                (multiple-value-bind (,vval idx)
                                    (deserialize-prim ,class ,vbuf ,vidx)
                                  (setq ,vidx idx)
-                                 (push (funcall #',(proto-deserializer msg) ,vval) ,temp))))))))
+                                 (push (funcall #',(proto-deserializer msg) ,vval) ,temp))))))
+                         (t
+                          (error 'undefined-field-type
+                                 :format-control "While generating the deserialize-object method ~
+                                                  for ~s,"
+                                 :format-arguments (list message)
+                                 :type-name (prin1-to-string class)
+                                 :field field))))
                   (t
                    (cond ((keywordp class)
                           (collect-deserializer
                                      (deserialize-prim ,class ,vbuf ,vidx)
                                    (let ((,vval (funcall #',(proto-deserializer msg) ,vval)))
                                      (setq ,vidx idx)
-                                     ,(write-slot vobj field vval)))))))))))))
+                                     ,(write-slot vobj field vval)))))))
+                         (t
+                          (error 'undefined-field-type
+                                 :format-control "While generating the deserialize-object method ~
+                                                  for ~s,"
+                                 :format-arguments (list message)
+                                 :type-name (prin1-to-string class)
+                                 :field field))))))))
       (let* ((rslots  (delete-duplicates rslots :key #'first))
              (rfields (mapcar #'first  rslots))
              (rtemps  (mapcar #'second rslots)))
                                     (tag   (make-tag class index)))
                                `(,iterator (,vval ,reader)
                                   (let ((,vval (funcall #',(proto-serializer msg) ,vval)))
-                                    (iincf ,vsize (prim-size ,vval ,class ,tag))))))))))
+                                    (iincf ,vsize (prim-size ,vval ,class ,tag)))))))
+                           (t
+                            (error 'undefined-field-type
+                                   :format-control "While generating the object-size method for ~s,"
+                                   :format-arguments (list message)
+                                   :type-name (prin1-to-string class)
+                                   :field field)))))
                   (t
                    (cond ((keywordp class)
                           (let ((tag (make-tag class index)))
                                 (when ,vval
                                   (iincf ,vsize (prim-size
                                                  (funcall #',(proto-serializer msg) ,vval)
-                                                 ,class ,tag)))))))))))))
+                                                 ,class ,tag)))))))
+                         (t
+                          (error 'undefined-field-type
+                                 :format-control "While generating the object-size method for ~s,"
+                                 :format-arguments (list message)
+                                 :type-name (prin1-to-string class)
+                                 :field field))))))))
       `(defmethod object-size
            (,vobj (,vclass (eql ,message)) &optional visited)
          (declare #.$optimize-serialization)
index 2e1dae7..98ca6fe 100644 (file)
                 ((:file "quick-tests")
                  (:static-file "golden.data")))
 
+     (:module "lisp-reference-tests"
+              :serial t
+              :pathname #p""
+              :components
+                ((:protobuf-file "package_test1") ; automatically includes package_test2
+                 (:protobuf-file "forward_reference")
+                 (:file "lisp-reference-tests")))
+
      ;; Google's own protocol buffers and protobuf definitions tests
      #+++notyet
      (:module "google-tests-proto"
diff --git a/tests/forward_reference.proto b/tests/forward_reference.proto
new file mode 100644 (file)
index 0000000..764db4d
--- /dev/null
@@ -0,0 +1,29 @@
+// Free Software published under an MIT-like license. See LICENSE
+//
+// Copyright (c) 2012 Google, Inc.  All rights reserved.
+//
+// Original author: Ben Wagner
+
+syntax = "proto2";
+
+package protobuf_forward_reference_unittest;
+
+message MessageWithForwardReference {
+  required MessageWithOverriddenLispClass foo = 1;
+  required EnumWithOverriddenLispClass bar = 2;
+}
+
+service ServiceWithForwardReference {
+  rpc Bloop(MessageWithOverriddenLispClass) returns (MessageWithForwardReference);
+  rpc Beep(MessageWithForwardReference) returns (MessageWithOverriddenLispClass);
+}
+
+message MessageWithOverriddenLispClass {
+  option (lisp_name) = "PROTOBUF-FORWARD-REFERENCE-UNITTEST:MSG-W-OVERRIDDEN-LISP-CLASS";
+  required int32 baz = 1;
+}
+
+enum EnumWithOverriddenLispClass {
+  option (lisp_name) = "PROTOBUF-FORWARD-REFERENCE-UNITTEST:ENUM-W-OVERRIDDEN-LISP-CLASS";
+  BAA = 1;
+}
diff --git a/tests/lisp-reference-tests.lisp b/tests/lisp-reference-tests.lisp
new file mode 100644 (file)
index 0000000..d73c4ae
--- /dev/null
@@ -0,0 +1,247 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;                                                                  ;;;
+;;; Free Software published under an MIT-like license. See LICENSE   ;;;
+;;;                                                                  ;;;
+;;; Copyright (c) 2012 Google, Inc.  All rights reserved.            ;;;
+;;;                                                                  ;;;
+;;; Original author: Ben Wagner                                      ;;;
+;;;                                                                  ;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(in-package "PROTO-TEST")
+
+(define-test cross-package-reference-test ()
+  (flet ((find-by-name (name proto-objects)
+           (find name proto-objects :key #'proto-name :test #'string=)))
+    (let* ((schema (find-schema :package_test1))
+           (message-with-cross-package-reference
+            (find-by-name "MessageWithCrossPackageReference" (proto-messages schema)))
+           (baz (find-by-name "baz" (proto-fields message-with-cross-package-reference)))
+           (bonk (find-by-name "bonk" (proto-fields message-with-cross-package-reference)))
+           (bam (find-by-name "bam" (proto-fields message-with-cross-package-reference)))
+           (bing (find-by-name "bing" (proto-fields message-with-cross-package-reference)))
+           (message-with-cross-package-extension
+            (find-by-name "MessageWithCrossPackageExtension" (proto-messages schema)))
+           (boo (find-by-name "boo" (proto-fields message-with-cross-package-extension)))
+           (service-with-cross-package-input-output
+            (find-by-name "ServiceWithCrossPackageInputOutput" (proto-services schema)))
+           (bloop (find-by-name "Bloop" (proto-methods service-with-cross-package-input-output)))
+           (beep (find-by-name "Beep" (proto-methods service-with-cross-package-input-output)))
+           (message-in-other-package-extend
+            (find-by-name "MessageInOtherPackage"
+                          (proto-messages message-with-cross-package-extension)))
+           (baa (find-by-name "baa" (proto-extended-fields message-in-other-package-extend))))
+      (assert-equal 'protobuf-package-unittest2::message-in-other-package
+                    (proto-class baz))
+      (assert-equal 'protobuf-package-unittest2::enum-in-other-package
+                    (proto-class bonk))
+      (assert-equal 'protobuf-package-unittest1::message-defined-in-both-packages
+                    (proto-class bam))
+      (assert-equal 'protobuf-package-unittest2::message-defined-in-both-packages
+                    (proto-class bing))
+      (assert-equal 'protobuf-package-unittest2::message-in-other-package
+                    (proto-class boo))
+      (assert-equal 'protobuf-package-unittest2::message-in-other-package
+                    (proto-input-type bloop))
+      (assert-equal 'protobuf-package-unittest1::message-with-cross-package-reference
+                    (proto-output-type bloop))
+      (assert-equal 'protobuf-package-unittest1::message-with-cross-package-reference
+                    (proto-input-type beep))
+      (assert-equal 'protobuf-package-unittest2::message-in-other-package
+                    (proto-output-type beep))
+      (assert-equal 'protobuf-package-unittest1::baa
+                    (proto-value baa))))
+
+  (let* ((orig1 (make-instance 'protobuf-package-unittest1::message-with-cross-package-reference
+                  :baz (make-instance 'protobuf-package-unittest2::message-in-other-package
+                         :foo 123)
+                  :bonk :bar
+                  :bam (make-instance 'protobuf-package-unittest1::message-defined-in-both-packages
+                         :boom "bomb")
+                  :bing (make-instance 'protobuf-package-unittest2::message-defined-in-both-packages
+                          :bang "gun")))
+         (orig2 (let ((extended-obj (make-instance 'protobuf-package-unittest2::message-in-other-package
+                                      :foo 123)))
+                  (setf (protobuf-package-unittest1::baa extended-obj) 456)
+                  (make-instance 'protobuf-package-unittest1::message-with-cross-package-extension
+                    :boo extended-obj)))
+         (bytes1 (serialize-object-to-bytes orig1
+                                            'protobuf-package-unittest1::message-with-cross-package-reference))
+         (bytes2 (serialize-object-to-bytes orig2
+                                            'protobuf-package-unittest1::message-with-cross-package-extension))
+         (new1 (deserialize-object 'protobuf-package-unittest1::message-with-cross-package-reference
+                                   bytes1))
+         (new2 (deserialize-object 'protobuf-package-unittest1::message-with-cross-package-extension
+                                   bytes2)))
+    (assert-true (typep (protobuf-package-unittest1::baz new1)
+                        'protobuf-package-unittest2::message-in-other-package))
+    (assert-equal 123
+                  (protobuf-package-unittest2::foo (protobuf-package-unittest1::baz new1)))
+    (assert-equal :bar
+                  (protobuf-package-unittest1::bonk new1))
+    (assert-equal "bomb"
+                  (protobuf-package-unittest1::boom (protobuf-package-unittest1::bam new1)))
+    (assert-equal "gun"
+                  (protobuf-package-unittest2::bang (protobuf-package-unittest1::bing new1)))
+    (assert-true (typep (protobuf-package-unittest1::boo new2)
+                        'protobuf-package-unittest2::message-in-other-package))
+    (assert-equal 123
+                  (protobuf-package-unittest2::foo (protobuf-package-unittest1::boo new2)))
+    (assert-equal 456
+                  (protobuf-package-unittest1::baa (protobuf-package-unittest1::boo new2)))))
+
+(define-test forward-reference-test ()
+  (flet ((find-by-name (name proto-objects)
+           (find name proto-objects :key #'proto-name :test #'string=)))
+    (let* ((schema (find-schema :forward_reference))
+           (message-with-forward-reference
+            (find-by-name "MessageWithForwardReference" (proto-messages schema)))
+           (foo (find-by-name "foo" (proto-fields message-with-forward-reference)))
+           (bar (find-by-name "bar" (proto-fields message-with-forward-reference)))
+           (service-with-forward-reference
+            (find-by-name "ServiceWithForwardReference" (proto-services schema)))
+           (bloop (find-by-name "Bloop" (proto-methods service-with-forward-reference)))
+           (beep (find-by-name "Beep" (proto-methods service-with-forward-reference))))
+      (assert-equal 'protobuf-forward-reference-unittest::msg-w-overridden-lisp-class
+                    (proto-class foo))
+      (assert-equal 'protobuf-forward-reference-unittest::ENUM-W-OVERRIDDEN-LISP-CLASS
+                    (proto-class bar))
+      (assert-equal 'protobuf-forward-reference-unittest::MSG-W-OVERRIDDEN-LISP-CLASS
+                    (proto-input-type bloop))
+      (assert-equal 'protobuf-forward-reference-unittest::MESSAGE-WITH-FORWARD-REFERENCE
+                    (proto-output-type bloop))
+      (assert-equal 'protobuf-forward-reference-unittest::MESSAGE-WITH-FORWARD-REFERENCE
+                    (proto-input-type beep))
+      (assert-equal 'protobuf-forward-reference-unittest::MSG-W-OVERRIDDEN-LISP-CLASS
+                    (proto-output-type beep))))
+  (let* ((orig (make-instance 'protobuf-forward-reference-unittest::message-with-forward-reference
+                 :foo (make-instance 'protobuf-forward-reference-unittest::msg-w-overridden-lisp-class
+                        :baz 123)
+                 :bar :baa))
+         (bytes (serialize-object-to-bytes orig
+                                           'protobuf-forward-reference-unittest::message-with-forward-reference))
+         (new (deserialize-object 'protobuf-forward-reference-unittest::message-with-forward-reference
+                                   bytes)))
+    (assert-true (typep (protobuf-forward-reference-unittest::foo new)
+                        'protobuf-forward-reference-unittest::msg-w-overridden-lisp-class))
+    (assert-equal 123
+                  (protobuf-forward-reference-unittest::baz (protobuf-forward-reference-unittest::foo new)))
+    (assert-equal :baa
+                  (protobuf-forward-reference-unittest::bar new))))
+
+(defparameter *test-proto-preamble*
+  "syntax = \"proto2\";
+
+package proto_test;
+
+message DefinedMessage {
+  optional string foo = 1;
+}
+
+")
+
+
+(define-test undefined-types-test ()
+  (labels ((parse-schema-containing (string)
+             (with-input-from-string (s (concatenate 'string *test-proto-preamble* string))
+               (parse-schema-from-stream s
+                                         ;; Parsing from a string doesn't produce a name, so supply
+                                         ;; it
+                                         :name "proto_test"
+                                         :class 'dummy
+                                         :conc-name nil)))
+           (parse-message-with-field-type (type)
+             (parse-schema-containing (format nil "message MessageWithUndefinedFieldType {~%~
+                                                   ~&  optional ~a bar = 1;~%~
+                                                   }~%" type)))
+           (parse-service-with-rpc (rpc)
+             (parse-schema-containing (format nil "service ServiceWithUndefinedMethodType {~%~
+                                                   ~&  ~a~%~
+                                                   }~%" rpc)))
+           (poor-mans-assert-regex-equal (expected-strings actual-string)
+             (assert-true
+              (loop with index = 0
+                    for expected-string in expected-strings
+                    as position = (search expected-string actual-string :start2 index)
+                    always position
+                    do (setf index (+ position (length expected-string))))))
+           (do-field-test (field-type)
+             (let ((condition (assert-error undefined-field-type
+                                (parse-message-with-field-type field-type))))
+               (poor-mans-assert-regex-equal
+                (list "Undefined type: Field #<"
+                      "PROTOBUF-FIELD PROTOBUFS-TEST::BAR :: NIL = 1"
+                      "in message #<"
+                      "PROTOBUF-MESSAGE PROTOBUFS-TEST::MESSAGE-WITH-UNDEFINED-FIELD-TYPE"
+                      (format nil "has unknown type \"~a\"." field-type))
+                (princ-to-string condition))
+               (assert-equal field-type (error-type-name condition))
+               (assert-equal "bar" (proto-name (error-field condition)))))
+           (method-test-assertions (condition where method-lisp-name method-proto-name type)
+             (poor-mans-assert-regex-equal
+              (list (format nil "Undefined type: ~a type for rpc #<" where)
+                    (format nil "PROTOBUF-METHOD PROTOBUFS-TEST::~a" method-lisp-name)
+                    "in service #<"
+                    "PROTOBUF-SERVICE ServiceWithUndefinedMethodType"
+                    (format nil "has unknown type \"~a\"." type))
+              (princ-to-string condition))
+             (assert-equal type (error-type-name condition))
+             (assert-equal method-proto-name (proto-name (error-method condition))))
+           (do-method-input-test (input-type)
+             (let ((condition (assert-error undefined-input-type
+                                (parse-service-with-rpc
+                                 (format nil "rpc MethodWithUndefinedInput (~a) ~
+                                              returns (DefinedMessage);" input-type)))))
+               (method-test-assertions condition "Input" "METHOD-WITH-UNDEFINED-INPUT"
+                                       "MethodWithUndefinedInput" input-type)))
+           (do-method-output-test (output-type)
+             (let ((condition (assert-error undefined-output-type
+                                (parse-service-with-rpc
+                                 (format nil "rpc MethodWithUndefinedOutput (DefinedMessage) ~
+                                              returns (~a);" output-type)))))
+               (method-test-assertions condition "Output" "METHOD-WITH-UNDEFINED-OUTPUT"
+                                       "MethodWithUndefinedOutput" output-type)))
+           (do-method-stream-test (stream-type)
+             (let ((condition (assert-error undefined-stream-type
+                                (parse-service-with-rpc
+                                 (format nil "rpc MethodWithUndefinedStream (DefinedMessage) ~
+                                              returns (DefinedMessage) {~
+                                              ~&    option stream_type = \"~a\";~
+                                              ~&  };" stream-type)))))
+               (method-test-assertions condition "Stream" "METHOD-WITH-UNDEFINED-STREAM"
+                                       "MethodWithUndefinedStream" stream-type))))
+
+    (parse-message-with-field-type "int32")
+    (do-field-test "int")
+    (parse-message-with-field-type "DefinedMessage")
+    (do-field-test "UndefinedMessage")
+    (do-field-test "other_package.DefinedMessage")
+
+    (parse-service-with-rpc
+     "rpc MethodWithDefinedInputOutput (DefinedMessage) returns (DefinedMessage);")
+    (do-method-input-test "UndefinedMessage")
+    ;; my understanding is that primitive types are not allowed for method input/output; if this is
+    ;; incorrect, change to "int"
+    (do-method-input-test "int32")
+    (do-method-input-test "other_package.DefinedMessage")
+
+    (do-method-output-test "UndefinedMessage")
+    (do-method-output-test "int32")
+    (do-method-output-test "other_package.DefinedMessage")
+
+    ;; stream_type is required to be fully qualified
+    (parse-service-with-rpc (format nil "rpc MethodWithDefinedInputOutput (DefinedMessage) ~
+                                         returns (DefinedMessage) {~
+                                         ~&    option stream_type = \"proto_test.DefinedMessage\";~
+                                         ~&  };"))
+    (do-method-stream-test "proto_test.UndefinedMessage")
+    (do-method-stream-test "int32")
+    (do-method-stream-test "other_package.DefinedMessage")))
+
+
+(define-test-suite lisp-reference-tests ()
+  (cross-package-reference-test
+   forward-reference-test
+   undefined-types-test))
+
+(register-test 'lisp-reference-tests)
diff --git a/tests/package_test1.proto b/tests/package_test1.proto
new file mode 100644 (file)
index 0000000..d997350
--- /dev/null
@@ -0,0 +1,34 @@
+// Free Software published under an MIT-like license. See LICENSE
+//
+// Copyright (c) 2012 Google, Inc.  All rights reserved.
+//
+// Original author: Ben Wagner
+
+syntax = "proto2";
+
+import "package_test2.proto";
+
+package protobuf_package_unittest1;
+
+message MessageDefinedInBothPackages {
+  required string boom = 1;
+}
+
+message MessageWithCrossPackageReference {
+  required MessageInOtherPackage baz = 1;
+  required EnumInOtherPackage bonk = 2;
+  required MessageDefinedInBothPackages bam = 3;
+  required protobuf_package_unittest2.MessageDefinedInBothPackages bing = 5;
+}
+
+message MessageWithCrossPackageExtension {
+  extend MessageInOtherPackage {
+    required int32 baa = 1000;
+  }
+  required MessageInOtherPackage boo = 1;
+}
+
+service ServiceWithCrossPackageInputOutput {
+  rpc Bloop(MessageInOtherPackage) returns (MessageWithCrossPackageReference);
+  rpc Beep(MessageWithCrossPackageReference) returns (MessageInOtherPackage);
+}
diff --git a/tests/package_test2.proto b/tests/package_test2.proto
new file mode 100644 (file)
index 0000000..8554917
--- /dev/null
@@ -0,0 +1,22 @@
+// Free Software published under an MIT-like license. See LICENSE
+//
+// Copyright (c) 2012 Google, Inc.  All rights reserved.
+//
+// Original author: Ben Wagner
+
+syntax = "proto2";
+
+package protobuf_package_unittest2;
+
+message MessageInOtherPackage {
+  required int32 foo = 1;
+  extensions 1000 to max;
+}
+
+enum EnumInOtherPackage {
+  BAR = 1;
+}
+
+message MessageDefinedInBothPackages {
+  required string bang = 1;
+}
index 4b7fd67..74174a4 100644 (file)
@@ -26,7 +26,8 @@
    "RUN-TEST"
    "ASSERT-EQUAL"
    "ASSERT-TRUE"
-   "ASSERT-FALSE"))
+   "ASSERT-FALSE"
+   "ASSERT-ERROR"))
 
 (defpackage protobuf-unittest
   (:use :common-lisp :protobufs)
@@ -39,3 +40,6 @@
 (defpackage protobuf-geodata
   (:use :common-lisp :protobufs)
   (:nicknames :geodata))
+
+(defpackage protobuf-forward-reference-unittest
+  (:use :common-lisp :protobufs))
index 3b426b4..bbcab3a 100644 (file)
@@ -46,7 +46,7 @@
     (format t "~&Running test ~A" test)
     (funcall test)))
 
-(defmacro assert-equal (actual expected &key (test 'eql))
+(defmacro assert-equal (actual expected &key (test 'equal))
   `(unless (,test ,actual ,expected)
      (warn "The value ~S is not equal to the expected value ~S"
            ',actual ',expected)))
   `(when ,form
      (warn "The value ~S does not evaluate to 'false'"
            ',form)))
+
+(defmacro assert-error (condition &body body)
+  "Checks if BODY signals a condition of class CONDITION. If it does not, a failure is
+   reported. If it is, the condition is caught and the condition object returned so that the test
+   can perform further checks on the condition object."
+  (let ((c (gensym "C")))
+    `(handler-case (progn ,@body)
+       (,condition (,c)
+         ,c)
+       (:no-error ()
+         (warn "Expected condition ~a while evaluating~{ ~s~}" ',condition ',body)))))
index 959552b..0a296ee 100644 (file)
   (proto:define-message stable-add-color ()
     (wheel :type stable-color-wheel)
     (color :type stable-color))
+  (proto:define-message string-primitive ()
+    (string :type string))
   (proto:define-service stable-color-wheel ()
-    (get-stable-color (string => stable-color))
+    (get-stable-color (string-primitive => stable-color))
     (set-stable-color (stable-color => stable-color)
                       :options (:deadline 1.0))))
 
@@ -73,8 +75,12 @@ message StableAddColor {
   required StableColor color = 2;
 }
 
+message StringPrimitive {
+  required string string = 1;
+}
+
 service StableColorWheel {
-  rpc GetStableColor (String) returns (StableColor);
+  rpc GetStableColor (StringPrimitive) returns (StableColor);
   rpc SetStableColor (StableColor) returns (StableColor) {
     option deadline = 1.0;
   }
index eacfb08..89795a3 100644 (file)
                                      (doseq (v (read-slot object slot reader))
                                        (let ((v (funcall (proto-serializer msg) v)))
                                          (print-prim v type field stream
-                                                     (or suppress-line-breaks indent))))))))
+                                                     (or suppress-line-breaks indent))))))
+                                  (t
+                                   (error 'undefined-field-type
+                                          :format-control "While printing ~s to text format,"
+                                          :format-arguments (list object)
+                                          :type-name (prin1-to-string type)
+                                          :field field))))
                            (t
                             (cond ((eq type :bool)
                                    (let ((v (cond ((or (eq (proto-required field) :required)
                                        (let ((v    (funcall (proto-serializer msg) v))
                                              (type (proto-proto-type msg)))
                                          (print-prim v type field stream
-                                                     (or suppress-line-breaks indent)))))))))))))
+                                                     (or suppress-line-breaks indent))))))
+                                  (t
+                                   (error 'undefined-field-type
+                                          :format-control "While printing ~s to text format,"
+                                          :format-arguments (list object)
+                                          :type-name (prin1-to-string type)
+                                          :field field)))))))))
         (declare (dynamic-extent #'do-field))
         (if print-name
           (if suppress-line-breaks
                                        (when slot
                                          (pushnew slot rslots)
                                          (push (funcall (proto-deserializer msg) val)
-                                               (slot-value object slot))))))))
+                                               (slot-value object slot))))))
+                                  (t
+                                   (error 'undefined-field-type
+                                          :format-control "While parsing ~s from text format,"
+                                          :format-arguments (list message)
+                                          :type-name (prin1-to-string type)
+                                          :field field))))
                            (t
                             (cond ((keywordp type)
                                    (expect-char stream #\:)
                                                   (otherwise (parse-signed-int stream)))))
                                        (when slot
                                          (setf (slot-value object slot)
-                                               (funcall (proto-deserializer msg) val)))))))))))))))
+                                               (funcall (proto-deserializer msg) val))))))
+                                  (t
+                                   (error 'undefined-field-type
+                                          :format-control "While parsing ~s from text format,"
+                                          :format-arguments (list message)
+                                          :type-name (prin1-to-string type)
+                                          :field field)))))))))))
     (declare (dynamic-extent #'deserialize))
     (deserialize (proto-class message) message)))