;;; ;;;
;;; Free Software published under an MIT-like license. See LICENSE ;;;
;;; ;;;
-;;; Copyright (c) 2012 Google, Inc. All rights reserved. ;;;
+;;; Copyright (c) 2012-2013 Google, Inc. All rights reserved. ;;;
;;; ;;;
;;; Original author: Scott McKay ;;;
;;; ;;;
collect ch into string
finally (progn
(skip-whitespace stream)
- (return (coerce string 'string)))))
+ (if (eql (peek-char nil stream nil) ch0)
+ ;; If the next character is a quote character, that means
+ ;; we should go parse another string and concatenate it
+ (return (strcat (coerce string 'string) (parse-string stream)))
+ (return (coerce string 'string))))))
(defun unescape-char (stream)
"Parse the next \"escaped\" character from the stream."
:class (or class (kintern (pathname-name (pathname stream))))
:conc-name conc-name))))
-(defun make-source-location (stream)
+;; 'with-proto-source-location' counts on this being a 3-element list
+;; Yeah, it's a kludge, but we really don't need anything complicated for this
+(defstruct (source-location (:type list) (:constructor %make-source-location))
+ pathname
+ start-pos
+ end-pos)
+
+(defun make-source-location (stream start end)
"Create a \"source locator\" for the stream at the current position.
With any luck, we can get meta-dot to pay attention to it."
+ (declare (ignore stream))
;; Don't record source locations if we're not parsing from a file
(and *protobuf-pathname*
- (list *protobuf-pathname* (file-position stream))))
+ (%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
:class class
:name name))
(*protobuf* schema)
- (*protobuf-package* *package*)
+ *protobuf-package*
+ *protobuf-rpc-package*
(*protobuf-conc-name* conc-name))
- (loop
- (skip-whitespace stream)
- (maybe-skip-comments stream)
- (let ((char (peek-char nil stream nil)))
- (cond ((null char)
- (remove-options schema "lisp_package")
- (return-from parse-schema-from-stream schema))
- ((proto-token-char-p char)
- (let ((token (parse-token stream)))
- (cond ((string= token "syntax")
- (parse-proto-syntax stream schema))
- ((string= token "package")
- (parse-proto-package stream schema))
- ((string= token "import")
- (parse-proto-import stream schema))
- ((string= token "option")
- (let* ((option (parse-proto-option stream schema))
- (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) *protobuf-package*)))
- (setf (proto-lisp-package schema) value)
- (setq *protobuf-package* package)))))
- ((string= token "enum")
- (parse-proto-enum stream schema))
- ((string= token "extend")
- (parse-proto-extend stream schema))
- ((string= token "message")
- (parse-proto-message stream schema))
- ((string= token "service")
- (parse-proto-service stream schema)))))
- (t
- (error "Syntax error at position ~D" (file-position stream))))))))
+ (labels ((ensure-package ()
+ "Find a fallback for our Lisp package if we don't have an obvious one already.
+ * java_package
+ * *package*"
+ (unless *protobuf-package*
+ (let ((java-package (find-option schema "java_package")))
+ (if java-package
+ (set-lisp-package schema java-package)
+ (setq *protobuf-package* *package*)))))
+ (ensure-rpc-package ()
+ (ensure-package)
+ (unless *protobuf-rpc-package*
+ (let ((rpc-package-name (format nil "~A-~A" (package-name *protobuf-package*) 'rpc)))
+ (setq *protobuf-rpc-package*
+ (or (find-proto-package rpc-package-name)
+ (make-package (string-upcase rpc-package-name) :use ())))))))
+ (loop
+ (skip-whitespace stream)
+ (maybe-skip-comments stream)
+ (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)))
+ (cond ((string= token "syntax")
+ (parse-proto-syntax stream schema))
+ ((string= token "package")
+ (parse-proto-package stream schema))
+ ((string= token "import")
+ (parse-proto-import stream schema))
+ ((string= token "option")
+ (let* ((option (parse-proto-option stream schema))
+ (name (and option (proto-name option)))
+ (value (and option (proto-value option))))
+ (when (and option (option-name= name "lisp_package"))
+ (set-lisp-package schema value))))
+ ((string= token "enum")
+ (ensure-package)
+ (parse-proto-enum stream schema))
+ ((string= token "extend")
+ (ensure-package)
+ (parse-proto-extend stream schema))
+ ((string= token "message")
+ (ensure-package)
+ (parse-proto-message stream schema))
+ ((string= token "service")
+ (ensure-rpc-package)
+ (parse-proto-service stream schema)))))
+ (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'.
(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'.
- Updates the 'protobuf-schema' object to use the package."
+ Updates the 'protobuf-schema' object to use the import."
(check-type schema protobuf-schema)
(let ((import (prog1 (parse-string stream)
- (expect-char stream terminator () "package")
+ (expect-char stream terminator () "import")
(maybe-skip-comments stream))))
(process-imports schema (list import))
(setf (proto-imports schema) (nconc (proto-imports schema) (list import)))))
(parse-string 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 options
+ (parse-text-format message :stream stream :parse-name nil)
+ ;; Who knows what to do? Skip the value
+ (skip-field stream))))
(t (kintern (parse-token stream)))))
(setq terminator (expect-char stream terminators () "option"))
(maybe-skip-comments stream)))
"Parse a Protobufs 'enum' from 'stream'.
Updates the 'protobuf-schema' or 'protobuf-message' object to have the enum."
(check-type protobuf (or protobuf-schema protobuf-message))
- (let* ((loc (make-source-location stream))
+ (let* ((loc (file-position stream))
(name (prog1 (parse-token stream)
(expect-char stream #\{ () "enum")
(maybe-skip-comments stream)))
:name name
:qualified-name (make-qualified-name protobuf name)
:parent protobuf
- :source-location loc)))
+ :source-location (make-source-location stream loc (i+ loc (length name))))))
(loop
(let ((name (parse-token stream)))
(when (null name)
(return-from parse-proto-enum enum))
(if (string= name "option")
(parse-proto-option stream enum)
- (parse-proto-enum-value stream enum name))))))
+ (parse-proto-enum-value stream protobuf enum name))))))
-(defun parse-proto-enum-value (stream enum name)
+(defun parse-proto-enum-value (stream protobuf enum name)
"Parse a Protobufs enum value from 'stream'.
Updates the 'protobuf-enum' object to have the enum value."
+ (declare (ignore protobuf))
(check-type enum protobuf-enum)
(expect-char stream #\= () "enum")
(let* ((idx (prog1 (parse-signed-int stream)
(maybe-skip-comments stream)))
(value (make-instance 'protobuf-enum-value
:name name
+ :qualified-name (make-qualified-name enum name)
:index idx
:value (proto->enum-name name *protobuf-package*)
:parent enum)))
"Parse a Protobufs 'message' from 'stream'.
Updates the 'protobuf-schema' or 'protobuf-message' object to have the message."
(check-type protobuf (or protobuf-schema protobuf-message))
- (let* ((loc (make-source-location stream))
+ (let* ((loc (file-position stream))
(name (prog1 (or name (parse-token stream))
(expect-char stream #\{ () "message")
(maybe-skip-comments stream)))
:parent protobuf
;; Maybe force accessors for all slots
:conc-name (conc-name-for-type class *protobuf-conc-name*)
- :source-location loc))
+ :source-location (make-source-location stream loc (i+ loc (length name)))))
(*protobuf* message))
(loop
(let ((token (parse-token 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."
(check-type protobuf (or protobuf-schema protobuf-message))
- (let* ((loc (make-source-location stream))
+ (let* ((loc (file-position stream))
(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
:class (proto-class message)
:name (proto-name message)
:qualified-name (proto-qualified-name message)
- :parent (proto-parent message)
+ :parent protobuf
:alias-for (proto-alias-for message)
:conc-name (proto-conc-name message)
:enums (copy-list (proto-enums message))
:fields (copy-list (proto-fields message))
:extensions (copy-list (proto-extensions message))
:message-type :extends ;this message is an extension
- :source-location loc)))
+ :source-location (make-source-location stream loc (i+ loc (length name))))))
(*protobuf* extends))
(assert message ()
"There is no message named ~A to extend" name)
(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)
"Parse a Protobufs 'service' from 'stream'.
Updates the 'protobuf-schema' object to have the service."
(check-type schema protobuf-schema)
- (let* ((loc (make-source-location stream))
+ (let* ((loc (file-position stream))
(name (prog1 (parse-token stream)
(expect-char stream #\{ () "service")
(maybe-skip-comments stream)))
:name name
:qualified-name (make-qualified-name *protobuf* name)
:parent schema
- :source-location loc))
+ :source-location (make-source-location stream loc (i+ loc (length name)))))
(index 0))
(loop
(let ((token (parse-token stream)))
(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."
(check-type service protobuf-service)
- (let* ((loc (make-source-location stream))
+ (let* ((loc (file-position stream))
(name (parse-token stream))
(in (prog2 (expect-char stream #\( () "service")
(parse-token stream)
(expect-char stream #\) () "service")))
- (ret (parse-token stream))
+ (ret (parse-token stream)) ;should be "=>"
(out (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) #\;))
+ (opts (multiple-value-bind (opts bodyp)
+ (parse-proto-method-options stream)
+ (when (or (not bodyp) (eql (peek-char nil stream nil) #\;))
(expect-char stream #\; () "service"))
(maybe-skip-comments stream)
opts))
: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
:index index
:options opts
- :source-location loc)))
+ :source-location (make-source-location stream loc (i+ loc (length name))))))
+ (assert (string= ret "returns") ()
+ "Syntax error in 'message' at position ~D" (file-position stream))
(let* ((name (find-option method "lisp_name"))
(stub (or (and name (make-lisp-symbol name))
stub)))
(setf (proto-class method) stub
- (proto-client-stub method) stub
- (proto-server-stub method) (intern (format nil "~A-~A" 'do stub) *protobuf-package*)))
- (assert (string= ret "returns") ()
- "Syntax error in 'message' at position ~D" (file-position stream))
+ (proto-client-stub method) (intern (format nil "~A-~A" 'call stub) *protobuf-rpc-package*)
+ (proto-server-stub method) (intern (format nil "~A-~A" stub 'impl) *protobuf-rpc-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."
+ Returns a list of 'protobuf-option' objects.
+ If a body was parsed, returns a second value T."
(when (eql (peek-char nil stream nil) #\{)
(expect-char stream #\{ () "service")
(maybe-skip-comments stream)
(collect-option (parse-proto-option stream nil)))
(expect-char stream #\} '(#\;) "service")
(maybe-skip-comments stream)
- options)))
+ (values options t))))