]> asedeno.scripts.mit.edu Git - cl-protobufs.git/blob - parser.lisp
Make the schema printer aware of qualified names.
[cl-protobufs.git] / parser.lisp
1 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2 ;;;                                                                  ;;;
3 ;;; Free Software published under an MIT-like license. See LICENSE   ;;;
4 ;;;                                                                  ;;;
5 ;;; Copyright (c) 2012 Google, Inc.  All rights reserved.            ;;;
6 ;;;                                                                  ;;;
7 ;;; Original author: Scott McKay                                     ;;;
8 ;;;                                                                  ;;;
9 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
10
11 (in-package "PROTO-IMPL")
12
13
14 ;;; .proto file parsing
15
16 ;;; Parsing utilities
17
18 (declaim (inline proto-whitespace-char-p))
19 (defun proto-whitespace-char-p (ch)
20   (declare #.$optimize-fast-unsafe)
21   (and ch (member ch '(#\space #\tab #\return #\newline))))
22
23 (declaim (inline proto-eol-char-p))
24 (defun proto-eol-char-p (ch)
25   (declare #.$optimize-fast-unsafe)   
26   (and ch (member ch '(#\return #\newline))))
27
28 (declaim (inline proto-token-char-p))
29 (defun proto-token-char-p (ch)
30   (declare #.$optimize-fast-unsafe)
31   (and ch (or (alpha-char-p ch)
32               (digit-char-p ch)
33               (member ch '(#\_ #\.)))))
34
35
36 (defun skip-whitespace (stream)
37   "Skip all the whitespace characters that are coming up in the stream."
38   (loop for ch = (peek-char nil stream nil)
39         until (or (null ch) (not (proto-whitespace-char-p ch)))
40         do (read-char stream nil)))
41
42 (defun expect-char (stream char &optional chars within)
43   "Expect to see 'char' as the next character in the stream; signal an error if it's not there.
44    Then skip all of the following whitespace.
45    The return value is the character that was eaten."
46   (let (ch)
47     (if (if (listp char)
48           (member (peek-char nil stream nil) char)
49           (eql (peek-char nil stream nil) char))
50       (setq ch (read-char stream))
51       (error "No '~C' found~@[ within '~A'~] at position ~D"
52              char within (file-position stream)))
53     (maybe-skip-chars stream chars)
54     ch))
55
56 (defun maybe-skip-chars (stream chars)
57   "Skip some optional characters in the stream,
58    then skip all of the following whitespace."
59   (skip-whitespace stream)
60   (when chars
61     (loop
62       (let ((ch (peek-char nil stream nil)))
63         (when (or (null ch) (not (member ch chars)))
64           (skip-whitespace stream)
65           (return-from maybe-skip-chars)))
66       (read-char stream))))
67
68
69 ;;--- Collect the comment so we can attach it to its associated object
70 (defun maybe-skip-comments (stream)
71   "If what appears next in the stream is a comment, skip it and any following comments,
72    then skip any following whitespace."
73   (loop
74     (let ((ch (peek-char nil stream nil)))
75       (when (or (null ch) (not (eql ch #\/)))
76         (return-from maybe-skip-comments))
77       (read-char stream)
78       (case (peek-char nil stream nil)
79         ((#\/)
80          (skip-line-comment stream))
81         ((#\*)
82          (skip-block-comment stream))
83         ((nil)
84          (skip-whitespace stream)
85          (return-from maybe-skip-comments))
86         (otherwise
87          (error "Found a '~C' at position ~D to start a comment, but no following '~C' or '~C'"
88                 #\/ (file-position stream) #\/ #\*))))))
89
90 (defun skip-line-comment (stream)
91   "Skip to the end of a line comment, that is, to the end of the line.
92    Then skip any following whitespace."
93   (loop for ch = (read-char stream nil)
94         until (or (null ch) (proto-eol-char-p ch)))
95   (skip-whitespace stream))
96
97 (defun skip-block-comment (stream)
98   "Skip to the end of a block comment, that is, until a '*/' is seen.
99    Then skip any following whitespace."
100   (loop for ch = (read-char stream nil)
101         do (cond ((null ch)
102                   (error "Premature end of file while skipping block comment"))
103                  ((and (eql ch #\*)
104                        (eql (peek-char nil stream nil) #\/))
105                   (read-char stream nil)
106                   (return))))
107   (skip-whitespace stream))
108
109
110 (defun parse-token (stream &optional additional-chars)
111   "Parse the next token in the stream, then skip the following whitespace.
112    The returned value is the token."
113   (when (let ((ch (peek-char nil stream nil)))
114           (or (proto-token-char-p ch) (member ch additional-chars)))
115     (loop for ch = (read-char stream nil)
116           for ch1 = (peek-char nil stream nil)
117           collect ch into token
118           until (or (null ch1)
119                     (and (not (proto-token-char-p ch1))
120                          (not (member ch1 additional-chars))))
121           finally (progn
122                     (skip-whitespace stream)
123                     (return (coerce token 'string))))))
124
125 (defun parse-parenthesized-token (stream)
126   "Parse the next token in the stream, then skip the following whitespace.
127    The token might be surrounded by parentheses.
128    The returned value is the token."
129   (let ((left (peek-char nil stream nil)))
130     (when (eql left #\()
131       (read-char stream))
132     (when (proto-token-char-p (peek-char nil stream nil))
133       (loop for ch = (read-char stream nil)
134             for ch1 = (peek-char nil stream nil)
135             collect ch into token
136             until (or (null ch1) (not (proto-token-char-p ch1)))
137             finally (progn
138                       (skip-whitespace stream)
139                       (when (eql left #\()
140                         (expect-char stream #\)))
141                       (return (coerce token 'string)))))))
142
143 (defun parse-token-or-string (stream)
144   (if (eql (peek-char nil stream nil) #\")
145     (values (parse-string stream) 'string)
146     (values (parse-token stream) 'symbol)))
147
148 (defun parse-string (stream)
149   "Parse the next quoted string in the stream, then skip the following whitespace.
150    The returned value is the string, without the quotation marks."
151   (loop with ch0 = (read-char stream nil)
152         for ch = (read-char stream nil)
153         until (or (null ch) (char= ch ch0))
154         when (eql ch #\\)
155           do (setq ch (unescape-char stream))
156         collect ch into string
157         finally (progn
158                   (skip-whitespace stream)
159                   (return (coerce string 'string)))))
160
161 (defun unescape-char (stream)
162   "Parse the next \"escaped\" character from the stream."
163   (let ((ch (read-char stream nil)))
164     (assert (not (null ch)) ()
165             "End of stream reached while reading escaped character")
166     (case ch
167       ((#\x)
168        ;; Two hex digits
169        (let* ((d1 (digit-char-p (read-char stream) 16))
170               (d2 (digit-char-p (read-char stream) 16)))
171          (code-char (+ (* d1 16) d2))))
172       ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9)
173        (if (not (digit-char-p (peek-char nil stream nil)))
174          #\null
175          ;; Three octal digits
176          (let* ((d1 (digit-char-p ch 8))
177                 (d2 (digit-char-p (read-char stream) 8))
178                 (d3 (digit-char-p (read-char stream) 8)))
179            (code-char (+ (* d1 64) (* d2 8) d3)))))
180       ((#\t) #\tab)
181       ((#\n) #\newline)
182       ((#\r) #\return)
183       ((#\f) #\page)
184       ((#\b) #\backspace)
185       ((#\a) #\bell)
186       ((#\e) #\esc)
187       (otherwise ch))))
188
189 (defun escape-char (ch)
190   "The inverse of 'unescape-char', for printing."
191   (if (and (standard-char-p ch) (graphic-char-p ch))
192     ch
193     (case ch
194       ((#\null)      "\\0")
195       ((#\tab)       "\\t")
196       ((#\newline)   "\\n")
197       ((#\return)    "\\r")
198       ((#\page)      "\\f")
199       ((#\backspace) "\\b")
200       ((#\bell)      "\\a")
201       ((#\esc)       "\\e")
202       (otherwise
203        (format nil "\\x~2,'0X" (char-code ch))))))
204
205 (defun parse-signed-int (stream)
206   "Parse the next token in the stream as an integer, then skip the following whitespace.
207    The returned value is the integer."
208   (let* ((sign (if (eql (peek-char nil stream nil) #\-)
209                  (progn (read-char stream) -1)
210                  1))
211          (int  (parse-unsigned-int stream)))
212     (* int sign)))
213
214 (defun parse-unsigned-int (stream)
215   "Parse the next token in the stream as an integer, then skip the following whitespace.
216    The returned value is the integer."
217   (when (digit-char-p (peek-char nil stream nil))
218     (loop for ch = (read-char stream nil)
219           for ch1 = (peek-char nil stream nil)
220           collect ch into token
221           until (or (null ch1) (and (not (digit-char-p ch1)) (not (eql ch #\x))))
222           finally (progn
223                     (skip-whitespace stream)
224                     (let ((token (coerce token 'string)))
225                       (if (starts-with token "0x")
226                         (let ((*read-base* 16))
227                           (return (parse-integer (subseq token 2))))
228                         (return (parse-integer token))))))))
229
230 (defun parse-float (stream)
231   "Parse the next token in the stream as a float, then skip the following whitespace.
232    The returned value is the float."
233   (let ((number (parse-number stream)))
234     (when number
235       (coerce number 'float))))
236
237 (defun parse-number (stream)
238   (when (let ((ch (peek-char nil stream nil)))
239           (or (digit-char-p ch) (member ch '(#\- #\+ #\.))))
240     (let ((token (parse-token stream '(#\- #\+ #\.))))
241       (when token
242         (skip-whitespace stream)
243         (parse-numeric-string token)))))
244
245 (defun parse-numeric-string (string)
246   (cond ((starts-with string "0x")
247          (parse-integer (subseq string 2) :radix 16))
248         ((starts-with string "-0x")
249          (- (parse-integer (subseq string 3) :radix 16)))
250         (t
251          (read-from-string string))))
252
253
254 ;;; The parser itself
255
256 (defun parse-schema-from-file (filename &key name class (conc-name ""))
257   "Parses the named file as a .proto file, and returns the Protobufs schema."
258   (with-open-file (stream filename
259                    :direction :input
260                    :external-format :utf-8
261                    :element-type 'character)
262     (let ((*protobuf-pathname* (pathname stream))
263           (*compile-file-pathname* (pathname stream))
264           (*compile-file-truename* (truename stream)))
265       (parse-schema-from-stream stream
266                                 :name  (or name (class-name->proto (pathname-name (pathname stream))))
267                                 :class (or class (kintern (pathname-name (pathname stream))))
268                                 :conc-name conc-name))))
269
270 ;; The syntax for Protocol Buffers is so simple that it doesn't seem worth
271 ;; writing a sophisticated parser
272 ;; Note that we don't put the result into *all-schemas*; that's done in 'define-schema'
273 (defun parse-schema-from-stream (stream &key name class (conc-name ""))
274   "Parses a top-level .proto file from the stream 'stream'.
275    Returns the protobuf schema that describes the .proto file."
276   (let* ((schema (make-instance 'protobuf-schema
277                    :class class
278                    :name  name))
279          (*protobuf* schema)
280          (*protobuf-package* *package*)
281          (*protobuf-conc-name* conc-name))
282     (loop
283       (skip-whitespace stream)
284       (maybe-skip-comments stream)
285       (let ((char (peek-char nil stream nil)))
286         (cond ((null char)
287                (remove-options schema "lisp_package")
288                (return-from parse-schema-from-stream schema))
289               ((proto-token-char-p char)
290                (let ((token (parse-token stream)))
291                  (cond ((string= token "syntax")
292                         (parse-proto-syntax stream schema))
293                        ((string= token "package")
294                         (parse-proto-package stream schema))
295                        ((string= token "import")
296                         (parse-proto-import stream schema))
297                        ((string= token "option")
298                         (let* ((option (parse-proto-option stream schema))
299                                (name   (and option (proto-name option)))
300                                (value  (and option (proto-value option))))
301                           (when (and option (option-name= name "lisp_package"))
302                             (let ((package (or (find-proto-package value) *protobuf-package*)))
303                               (setf (proto-lisp-package schema) value)
304                               (setq *protobuf-package* package)))))
305                        ((string= token "enum")
306                         (parse-proto-enum stream schema))
307                        ((string= token "extend")
308                         (parse-proto-extend stream schema))
309                        ((string= token "message")
310                         (parse-proto-message stream schema))
311                        ((string= token "service")
312                         (parse-proto-service stream schema)))))
313               (t
314                (error "Syntax error at position ~D" (file-position stream))))))))
315
316 (defun parse-proto-syntax (stream schema &optional (terminator #\;))
317   "Parse a Protobufs syntax line from 'stream'.
318    Updates the 'protobuf-schema' object to use the syntax."
319   (let ((syntax (prog2 (expect-char stream #\= () "syntax")
320                     (parse-string stream)
321                   (expect-char stream terminator () "syntax")
322                   (maybe-skip-comments stream))))
323     (setf (proto-syntax schema) syntax)))
324
325 (defun parse-proto-package (stream schema &optional (terminator #\;))
326   "Parse a Protobufs package line from 'stream'.
327    Updates the 'protobuf-schema' object to use the package."
328   (check-type schema protobuf-schema)
329   (let* ((package  (prog1 (parse-token stream)
330                      (expect-char stream terminator () "package")
331                      (maybe-skip-comments stream)))
332          (lisp-pkg (or (proto-lisp-package schema)
333                        (substitute #\- #\_ package))))
334     (setf (proto-package schema) package)
335     (unless (proto-lisp-package schema)
336       (setf (proto-lisp-package schema) lisp-pkg))
337     (let ((package (or (find-proto-package lisp-pkg) *protobuf-package*)))
338       (setq *protobuf-package* package))))
339
340 (defun parse-proto-import (stream schema &optional (terminator #\;))
341   "Parse a Protobufs import line from 'stream'.
342    Updates the 'protobuf-schema' object to use the package."
343   (check-type schema protobuf-schema)
344   (let ((import (prog1 (parse-string stream)
345                   (expect-char stream terminator () "package")
346                   (maybe-skip-comments stream))))
347     (process-imports schema (list import))
348     (setf (proto-imports schema) (nconc (proto-imports schema) (list import)))))
349
350 (defun parse-proto-option (stream protobuf &optional (terminators '(#\;)))
351   "Parse a Protobufs option line from 'stream'.
352    Updates the 'protobuf-schema' (or message, service, method) to have the option."
353   (check-type protobuf (or null base-protobuf))
354   (let* (terminator
355          (key (prog1 (parse-parenthesized-token stream)
356                 (expect-char stream #\= () "option")))
357          (val (prog1 (let ((ch (peek-char nil stream nil)))
358                        (cond ((eql ch #\")
359                               (parse-string stream))
360                              ((or (digit-char-p ch) (member ch '(#\- #\+ #\.)))
361                               (parse-number stream))
362                              (t (kintern (parse-token stream)))))
363                 (setq terminator (expect-char stream terminators () "option"))
364                 (maybe-skip-comments stream)))
365          (option (make-instance 'protobuf-option
366                    :name  key
367                    :value val)))
368     (cond (protobuf
369            (setf (proto-options protobuf) (nconc (proto-options protobuf) (list option)))
370            (values option terminator))
371           (t
372            ;; If nothing to graft the option into, just return it as the value
373            (values option terminator)))))
374
375
376 (defun parse-proto-enum (stream protobuf)
377   "Parse a Protobufs 'enum' from 'stream'.
378    Updates the 'protobuf-schema' or 'protobuf-message' object to have the enum."
379   (check-type protobuf (or protobuf-schema protobuf-message))
380   (let* ((name (prog1 (parse-token stream)
381                  (expect-char stream #\{ () "enum")
382                  (maybe-skip-comments stream)))
383          (enum (make-instance 'protobuf-enum
384                  :class (proto->class-name name *protobuf-package*)
385                  :name name
386                  :qualified-name (make-qualified-name protobuf name))))
387     (loop
388       (let ((name (parse-token stream)))
389         (when (null name)
390           (expect-char stream #\} '(#\;) "enum")
391           (maybe-skip-comments stream)
392           (setf (proto-enums protobuf) (nconc (proto-enums protobuf) (list enum)))
393           (let ((type (find-option enum "lisp_name")))
394             (when type
395               (setf (proto-class enum) (make-lisp-symbol type))))
396           (let ((alias (find-option enum "lisp_alias")))
397             (when alias
398               (setf (proto-alias-for enum) (make-lisp-symbol alias))))
399           (return-from parse-proto-enum enum))
400         (if (string= name "option")
401           (parse-proto-option stream enum)
402           (parse-proto-enum-value stream enum name))))))
403
404 (defun parse-proto-enum-value (stream enum name)
405   "Parse a Protobufs enum value from 'stream'.
406    Updates the 'protobuf-enum' object to have the enum value."
407   (check-type enum protobuf-enum)
408   (expect-char stream #\= () "enum")
409   (let* ((idx  (prog1 (parse-signed-int stream)
410                  (expect-char stream #\; () "enum")
411                  (maybe-skip-comments stream)))
412          (value (make-instance 'protobuf-enum-value
413                   :name  name
414                   :index idx
415                   :value (proto->enum-name name *protobuf-package*))))
416     (setf (proto-values enum) (nconc (proto-values enum) (list value)))
417     value))
418
419
420 (defun parse-proto-message (stream protobuf &optional name)
421   "Parse a Protobufs 'message' from 'stream'.
422    Updates the 'protobuf-schema' or 'protobuf-message' object to have the message."
423   (check-type protobuf (or protobuf-schema protobuf-message))
424   (let* ((name (prog1 (or name (parse-token stream))
425                  (expect-char stream #\{ () "message")
426                  (maybe-skip-comments stream)))
427          (class (proto->class-name name *protobuf-package*))
428          (message (make-instance 'protobuf-message
429                     :class class
430                     :name  name
431                     :qualified-name (make-qualified-name protobuf name)
432                     :parent protobuf
433                     ;; Maybe force accessors for all slots
434                     :conc-name (conc-name-for-type class *protobuf-conc-name*)))
435          (*protobuf* message))
436     (loop
437       (let ((token (parse-token stream)))
438         (when (null token)
439           (expect-char stream #\} '(#\;) "message")
440           (maybe-skip-comments stream)
441           (setf (proto-messages protobuf) (nconc (proto-messages protobuf) (list message)))
442           (let ((type (find-option message "lisp_name")))
443             (when type
444               (setf (proto-class message) (make-lisp-symbol type))))
445           (let ((alias (find-option message "lisp_alias")))
446             (when alias
447               (setf (proto-alias-for message) (make-lisp-symbol alias))))
448           (return-from parse-proto-message message))
449         (cond ((string= token "enum")
450                (parse-proto-enum stream message))
451               ((string= token "extend")
452                (parse-proto-extend stream message))
453               ((string= token "message")
454                (parse-proto-message stream message))
455               ((member token '("required" "optional" "repeated") :test #'string=)
456                (parse-proto-field stream message token))
457               ((string= token "option")
458                (parse-proto-option stream message))
459               ((string= token "extensions")
460                (parse-proto-extension stream message))
461               (t
462                (error "Unrecognized token ~A at position ~D"
463                       token (file-position stream))))))))
464
465 (defun parse-proto-extend (stream protobuf)
466   "Parse a Protobufs 'extend' from 'stream'.
467    Updates the 'protobuf-schema' or 'protobuf-message' object to have the message."
468   (check-type protobuf (or protobuf-schema protobuf-message))
469   (let* ((name (prog1 (parse-token stream)
470                  (expect-char stream #\{ () "extend")
471                  (maybe-skip-comments stream)))
472          (message (find-message protobuf name))
473          (extends (and message
474                        (make-instance 'protobuf-message
475                          :class (proto-class message)
476                          :name  (proto-name message)
477                          :qualified-name (proto-qualified-name message)
478                          :parent (proto-parent message)
479                          :alias-for (proto-alias-for message)
480                          :conc-name (proto-conc-name message)
481                          :enums    (copy-list (proto-enums message))
482                          :messages (copy-list (proto-messages message))
483                          :fields   (copy-list (proto-fields message))
484                          :extensions (copy-list (proto-extensions message))
485                          :message-type :extends)))      ;this message is an extension
486          (*protobuf* extends))
487     (assert message ()
488             "There is no message named ~A to extend" name)
489     (loop
490       (let ((token (parse-token stream)))
491         (when (null token)
492           (expect-char stream #\} '(#\;) "extend")
493           (maybe-skip-comments stream)
494           (setf (proto-messages protobuf) (nconc (proto-messages protobuf) (list extends)))
495           (setf (proto-extenders protobuf) (nconc (proto-extenders protobuf) (list extends)))
496           (let ((type (find-option extends "lisp_name")))
497             (when type
498               (setf (proto-class extends) (make-lisp-symbol type))))
499           (let ((alias (find-option extends "lisp_alias")))
500             (when alias
501               (setf (proto-alias-for extends) (make-lisp-symbol alias))))
502           (return-from parse-proto-extend extends))
503         (cond ((member token '("required" "optional" "repeated") :test #'string=)
504                (let ((field (parse-proto-field stream extends token message)))
505                  (setf (proto-extended-fields extends) (nconc (proto-extended-fields extends) (list field)))))
506               ((string= token "option")
507                (parse-proto-option stream extends))
508               (t
509                (error "Unrecognized token ~A at position ~D"
510                       token (file-position stream))))))))
511
512 (defun parse-proto-field (stream message required &optional extended-from)
513   "Parse a Protobufs field from 'stream'.
514    Updates the 'protobuf-message' object to have the field."
515   (check-type message protobuf-message)
516   (let ((type (parse-token stream)))
517     (if (string= type "group")
518       (parse-proto-group stream message required extended-from)
519       (let* ((name (prog1 (parse-token stream)
520                      (expect-char stream #\= () "message")))
521              (idx  (parse-unsigned-int stream))
522              (opts (prog1 (parse-proto-field-options stream)
523                      (expect-char stream #\; () "message")
524                      (maybe-skip-comments stream)))
525              (packed (find-option opts "packed"))
526              (ptype  (if (member type '("int32" "int64" "uint32" "uint64" "sint32" "sint64"
527                                         "fixed32" "fixed64" "sfixed32" "sfixed64"
528                                         "string" "bytes" "bool" "float" "double") :test #'string=)
529                        (kintern type)
530                        type))
531              (class  (if (keywordp ptype) ptype (proto->class-name type *protobuf-package*)))
532              (slot   (proto->slot-name name *protobuf-package*))
533              (reqd   (kintern required))
534              (field  (make-instance 'protobuf-field
535                        :name  name
536                        :type  type
537                        :class class
538                        :qualified-name (make-qualified-name message name)
539                        ;; One of :required, :optional or :repeated
540                        :required reqd
541                        :index idx
542                        :value slot
543                        ;; Fields parsed from .proto files usually get an accessor
544                        :reader (let ((conc-name (proto-conc-name message)))
545                                  (and conc-name
546                                       (intern (format nil "~A~A" conc-name slot) *protobuf-package*)))
547                        :default (multiple-value-bind (default type default-p)
548                                     (find-option opts "default")
549                                   (declare (ignore type))
550                                   (if default-p
551                                     default
552                                     (if (eq reqd :repeated) $empty-list $empty-default)))
553                        :packed  (and packed (boolean-true-p packed))
554                        :message-type (proto-message-type message)
555                        :options (remove-options opts "default" "packed"))))
556         (when extended-from
557           (assert (index-within-extensions-p idx extended-from) ()
558                   "The index ~D is not in range for extending ~S"
559                   idx (proto-class extended-from)))
560         (let ((slot (find-option opts "lisp_name")))
561           (when slot
562             (setf (proto-value field) (make-lisp-symbol type))))
563         (setf (proto-fields message) (nconc (proto-fields message) (list field)))
564         field))))
565
566 (defun parse-proto-group (stream message required &optional extended-from)
567   "Parse a (deprecated) Protobufs group from 'stream'.
568    Updates the 'protobuf-message' object to have the group type and field."
569   (check-type message protobuf-message)
570   (let* ((type (prog1 (parse-token stream)
571                  (expect-char stream #\= () "message")))
572          (name (slot-name->proto (proto->slot-name type)))
573          (idx  (parse-unsigned-int stream))
574          (msg  (parse-proto-message stream message type))
575          (class (proto->class-name type *protobuf-package*))
576          (slot  (proto->slot-name name *protobuf-package*))
577          (field (make-instance 'protobuf-field
578                   :name  name
579                   :type  type
580                   :class class
581                   :qualified-name (make-qualified-name message name)
582                   :required (kintern required)
583                   :index idx
584                   :value slot
585                   ;; Groups parsed from .proto files usually get an accessor
586                   :reader (let ((conc-name (proto-conc-name message)))
587                             (and conc-name
588                                  (intern (format nil "~A~A" conc-name slot) *protobuf-package*)))
589                   :message-type :group)))
590     (setf (proto-message-type msg) :group)
591     (when extended-from
592       (assert (index-within-extensions-p idx extended-from) ()
593               "The index ~D is not in range for extending ~S"
594               idx (proto-class extended-from)))
595     (setf (proto-fields message) (nconc (proto-fields message) (list field)))
596     field))
597
598 (defun parse-proto-field-options (stream)
599   "Parse any options in a Protobufs field from 'stream'.
600    Returns a list of 'protobuf-option' objects."
601   (with-collectors ((options collect-option))
602     (let ((terminator nil))
603       (loop
604         (cond ((eql (peek-char nil stream nil) #\[)
605                (expect-char stream #\[ () "message"))
606               ((eql terminator #\,))
607               (t
608                (return-from parse-proto-field-options options)))
609         (multiple-value-bind (option term)
610             (parse-proto-option stream nil '(#\] #\,))
611           (setq terminator term)
612           (collect-option option))))))
613
614 (defun parse-proto-extension (stream message)
615   (check-type message protobuf-message)
616   (let* ((from  (parse-unsigned-int stream))
617          (token (parse-token stream))
618          (to    (let ((ch (peek-char nil stream nil)))
619                   (cond ((digit-char-p (peek-char nil stream nil))
620                          (parse-unsigned-int stream))
621                         ((eql ch #\;) from)
622                         (t (parse-token stream))))))
623     (expect-char stream #\; () "message")
624     (assert (or (null token) (string= token "to")) ()
625             "Expected 'to' in 'extensions' at position ~D" (file-position stream))
626     (assert (or (integerp to) (string= to "max")) ()
627             "Extension value is not an integer or 'max' as position ~D" (file-position stream))
628     (let ((extension (make-instance 'protobuf-extension
629                        :from from
630                        :to   (if (integerp to) to #.(1- (ash 1 29))))))
631       (setf (proto-extensions message)
632             (nconc (proto-extensions message)
633                    (list extension)))
634       extension)))
635
636
637 (defun parse-proto-service (stream schema)
638   "Parse a Protobufs 'service' from 'stream'.
639    Updates the 'protobuf-schema' object to have the service."
640   (check-type schema protobuf-schema)
641   (let* ((name (prog1 (parse-token stream)
642                  (expect-char stream #\{ () "service")
643                  (maybe-skip-comments stream)))
644          (service (make-instance 'protobuf-service
645                     :class (proto->class-name name *protobuf-package*)
646                     :name name
647                     :qualified-name (make-qualified-name *protobuf* name)))
648          (index 0))
649     (loop
650       (let ((token (parse-token stream)))
651         (when (null token)
652           (expect-char stream #\} '(#\;) "service")
653           (maybe-skip-comments stream)
654           (setf (proto-services schema) (nconc (proto-services schema) (list service)))
655           (return-from parse-proto-service service))
656         (cond ((string= token "option")
657                (parse-proto-option stream service))
658               ((string= token "rpc")
659                (parse-proto-method stream service (iincf index)))
660               (t
661                (error "Unrecognized token ~A at position ~D"
662                       token (file-position stream))))))))
663
664 (defun parse-proto-method (stream service index)
665   "Parse a Protobufs method from 'stream'.
666    Updates the 'protobuf-service' object to have the method."
667   (check-type service protobuf-service)
668   (let* ((name (parse-token stream))
669          (in   (prog2 (expect-char stream #\( () "service")
670                    (parse-token stream)
671                  (expect-char stream #\) () "service")))
672          (ret  (parse-token stream))
673          (out  (prog2 (expect-char stream #\( () "service")
674                    (parse-token stream)
675                  (expect-char stream #\) () "service")))
676          (opts (let ((opts (parse-proto-method-options stream)))
677                  (when (or (null opts) (eql (peek-char nil stream nil) #\;))
678                    (expect-char stream #\; () "service"))
679                  (maybe-skip-comments stream)
680                  opts))
681          (stub   (proto->class-name name *protobuf-package*))
682          (method (make-instance 'protobuf-method
683                    :class stub
684                    :name  name
685                    :qualified-name (make-qualified-name *protobuf* name)
686                    :input-type  (proto->class-name in *protobuf-package*)
687                    :input-name  in
688                    :output-type (proto->class-name out *protobuf-package*)
689                    :output-name out
690                    :index index
691                    :options opts)))
692     (let* ((name (find-option method "lisp_name"))
693            (stub (or (and name (make-lisp-symbol name))
694                      stub)))
695       (setf (proto-class method) stub
696             (proto-client-stub method) stub
697             (proto-server-stub method) (intern (format nil "~A-~A" 'do stub) *protobuf-package*)))
698     (assert (string= ret "returns") ()
699             "Syntax error in 'message' at position ~D" (file-position stream))
700     (setf (proto-methods service) (nconc (proto-methods service) (list method)))
701     method))
702
703 (defun parse-proto-method-options (stream)
704   "Parse any options in a Protobufs method from 'stream'.
705    Returns a list of 'protobuf-option' objects."
706   (when (eql (peek-char nil stream nil) #\{)
707     (expect-char stream #\{ () "service")
708     (maybe-skip-comments stream)
709     (with-collectors ((options collect-option))
710       (loop
711         (when (eql (peek-char nil stream nil) #\})
712           (return))
713         (assert (string= (parse-token stream) "option") ()
714                 "Syntax error in 'message' at position ~D" (file-position stream))
715         (collect-option (parse-proto-option stream nil)))
716       (expect-char stream #\} '(#\;) "service")
717       (maybe-skip-comments stream)
718       options)))