1 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
3 ;;; Confidential and proprietary information of ITA Software, Inc. ;;;
5 ;;; Copyright (c) 2012 ITA Software, Inc. All rights reserved. ;;;
7 ;;; Original author: Scott McKay ;;;
9 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
11 (in-package "PROTO-IMPL")
14 ;;; Protocol buffer defining macros
16 ;; Define a schema named 'name', corresponding to a .proto file of that name
17 (defmacro define-proto (name (&key proto-name syntax package import optimize options documentation)
18 &body messages &environment env)
19 "Define a schema named 'name', corresponding to a .proto file of that name.
20 'proto-name' can be used to override the defaultly generated name.
21 'syntax' and 'package' are as they would be in a .proto file.
22 'imports' is a list of pathname strings to be imported.
23 'options' is a property list, i.e., (\"key1\" \"val1\" \"key2\" \"val2\" ...).
24 The body consists of 'define-enum', 'define-message' or 'define-service' forms."
25 (with-collectors ((enums collect-enum)
29 (dolist (msg messages)
30 (assert (and (listp msg)
31 (member (car msg) '(define-enum define-message define-service))) ()
32 "The body of ~S must be one of ~{~S~^ or ~}"
33 'define-proto '(define-enum define-message define-service))
34 ;; The macro-expander will return a form that consists
35 ;; of 'progn' followed by a symbol naming what we've expanded
36 ;; (define-enum, define-message, define-service), followed by
37 ;; by a (optional) Lisp defining form (deftype, defclass),
38 ;; followed by a form that creates the model object
39 (destructuring-bind (&optional progn type model definers)
40 (macroexpand-1 msg env)
41 (assert (eq progn 'progn) ()
42 "The macroexpansion for ~S failed" msg)
43 (map () #'collect-form definers)
50 (collect-svc model)))))
51 (let ((vname (fintern "*~A*" name))
52 (pname (or proto-name (class-name->proto name)))
54 (options (loop for (key val) on options by #'cddr
55 collect `(make-instance 'protobuf-option
62 (protobuf (make-instance 'protobuf
65 :syntax ,(or syntax "proto2")
66 :package ,(if (stringp package) package (string-downcase (string package)))
67 :imports ',(if (listp import) import (list import))
68 :options (list ,@options)
71 :messages (list ,@msgs)
72 :services (list ,@svcs)
73 :documentation ,documentation)))
75 (multiple-value-bind (upgradable warnings)
76 (protobuf-upgradable old protobuf)
78 (protobufs-warn "The old schema for ~S (~A) can't be safely upgraded; proceeding anyway"
80 (map () #'protobufs-warn warnings))))
81 (setq ,vname protobuf)
82 (setf (gethash ',pname *all-protobufs*) protobuf)
83 (setf (gethash ',cname *all-protobufs*) protobuf)
86 ;; Define an enum type named 'name' and a Lisp 'deftype'
87 (defmacro define-enum (name (&key proto-name conc-name type options documentation)
89 "Define an enum type named 'name' and a Lisp 'deftype'.
90 'proto-name' can be used to override the defaultly generated Protobufs name.
91 'conc-name' will be used as the prefix to the Lisp enum names, if it's supplied.
92 If 'type' is given, no Lisp deftype is defined. This feature is intended to be used
93 to model enum types that already exist in Lisp.
94 'options' is a set of keyword/value pairs, both of which are strings.
95 The body consists of the enum values in the form (name &key index)."
96 (with-collectors ((vals collect-val)
101 (let* ((idx (if (listp val) (second val) (incf index)))
102 (name (if (listp val) (first val) val))
103 (val-name (kintern (if conc-name (format nil "~A~A" conc-name name) (symbol-name name))))
104 (enum-name (if conc-name (format nil "~A~A" conc-name name) (symbol-name name))))
105 (collect-val val-name)
106 (collect-eval `(make-instance 'protobuf-enum-value
107 :name ,(enum-name->proto enum-name)
109 :value ,val-name)))))
112 ;; If we've got a type override, define a type matching the Lisp name
113 ;; of this message so that typep and subtypep work
114 (unless (eq name type)
115 (collect-form `(deftype ,name () ',type)))
116 ;; If no type override, define the type now
117 (collect-form `(deftype ,name () '(member ,@vals))))
118 (let ((options (loop for (key val) on options by #'cddr
119 collect `(make-instance 'protobuf-option
124 (make-instance 'protobuf-enum
125 :name ,(or proto-name (class-name->proto name))
127 :class-override ',type
128 :options (list ,@options)
129 :values (list ,@evals)
130 :documentation ,documentation)
133 ;; Define a message named 'name' and a Lisp 'defclass'
134 (defmacro define-message (name (&key proto-name conc-name class options documentation)
135 &body fields &environment env)
136 "Define a message named 'name' and a Lisp 'defclass'.
137 'proto-name' can be used to override the defaultly generated Protobufs name.
138 The body consists of fields, or 'define-enum' or 'define-message' forms.
139 'conc-name' will be used as the prefix to the Lisp slot accessors, if it's supplied.
140 If 'class' is given, no Lisp class is defined. This feature is intended to be used
141 to model messages that will be serialized from existing Lisp classes; unless you
142 get the slot names correct in each field, it will be the case that trying to
143 deserialize into a Lisp object won't work.
144 'options' is a set of keyword/value pairs, both of which are strings.
145 Fields take the form (name &key type default reader)
146 'name' can be either a symbol giving the field name, or a list whose
147 first element is the field name and whose second element is the index."
148 (with-collectors ((enums collect-enum)
152 (forms collect-form))
154 (declare (type fixnum index))
157 ((define-enum define-message define-extension)
158 (destructuring-bind (&optional progn type model definers)
159 (macroexpand-1 fld env)
160 (assert (eq progn 'progn) ()
161 "The macroexpansion for ~S failed" fld)
162 (map () #'collect-form definers)
165 (collect-enum model))
169 (collect-msg model)))))
171 (when (i= index 18999) ;skip over the restricted range
173 (destructuring-bind (slot &key type default reader proto-name) fld
174 (let* ((idx (if (listp slot) (second slot) (iincf index)))
175 (slot (if (listp slot) (first slot) slot))
176 (reqd (clos-type-to-protobuf-required type))
177 (accessor (intern (if conc-name (format nil "~A~A" conc-name slot) (symbol-name slot))
178 (symbol-package slot))))
179 (multiple-value-bind (ptype pclass)
180 (clos-type-to-protobuf-type type)
182 (collect-slot `(,slot :type ,type
184 :initarg ,(kintern (symbol-name slot))
185 ,@(and default (list :initform default)))))
186 (collect-field `(make-instance 'protobuf-field
187 :name ,(or proto-name (slot-name->proto slot))
194 :default ,(and default (format nil "~A" default))
195 :packed ,(and (eq reqd :repeated)
196 (packed-type-p pclass)))))))))))
198 ;; If we've got a class override, define a type matching the Lisp name
199 ;; of this message so that typep and subtypep work
200 (unless (eq name class)
201 (collect-form `(deftype ,name () ',class)))
202 ;; If no class override, define the class now
203 (collect-form `(defclass ,name () (,@slots))))
204 (let ((options (loop for (key val) on options by #'cddr
205 collect `(make-instance 'protobuf-option
210 (make-instance 'protobuf-message
211 :name ,(or proto-name (class-name->proto name))
213 :class-override ',class
214 :conc-name ,(and conc-name (string conc-name))
215 :options (list ,@options)
216 :enums (list ,@enums)
217 :messages (list ,@msgs)
218 :fields (list ,@flds)
219 :documentation ,documentation)
222 (defmacro define-extension (from to)
223 "Define an extension range within a message.
224 The \"body\" is the start and end of the range, both inclusive."
227 (make-instance 'protobuf-extension
232 ;; Define a service named 'name' with generic functions declared for
233 ;; each of the RPCs within the service
234 (defmacro define-service (name (&key proto-name options documentation)
236 "Define a service named 'name' and a Lisp 'defgeneric'.
237 'proto-name' can be used to override the defaultly generated Protobufs name.
238 'options' is a set of keyword/value pairs, both of which are strings.
239 The body is a set of RPC specs of the form (name (input-type output-type) &key options)."
240 (with-collectors ((rpcs collect-rpc)
241 (forms collect-form))
242 (dolist (rpc rpc-specs)
243 (destructuring-bind (name (input-class output-class) &key options) rpc
244 (let ((options (loop for (key val) on options by #'cddr
245 collect `(make-instance 'protobuf-option
248 (collect-rpc `(make-instance 'protobuf-rpc
249 :name ,(class-name->proto name)
251 :input-type ,(and input-class (class-name->proto input-class))
252 :input-class ',input-class
253 :output-type ,(and output-class (class-name->proto output-class))
254 :output-class ',output-class
255 :options (list ,@options)))
256 ;;--- Is this really all we need as the stub for the RPC?
257 (collect-form `(defgeneric ,name (,@(and input-class (list input-class)))
258 (declare (values ,output-class)))))))
259 (let ((options (loop for (key val) on options by #'cddr
260 collect `(make-instance 'protobuf-option
265 (make-instance 'protobuf-service
266 :name ,(or proto-name (class-name->proto name))
268 :options (list ,@options)
270 :documentation ,documentation)
274 ;;; Ensure everything in a Protobufs schema is defined
276 (defvar *undefined-messages*)
278 ;; A very useful tool during development...
279 (defun ensure-all-protobufs ()
282 (loop for p being the hash-values of *all-protobufs*
284 #'string< :key #'proto-name)))
285 (mapcan #'ensure-protobuf protos)))
287 (defmethod ensure-protobuf ((proto protobuf))
288 "Ensure that all of the types are defined in the Protobufs schema 'proto'.
289 This returns two values:
290 - A list whose elements are (<undefined-type> \"message:field\" ...)
291 - The accumulated warnings table that has the same information as objects."
292 (let ((*undefined-messages* (make-hash-table))
293 (trace (list proto)))
294 (map () (curry #'ensure-message trace) (proto-messages proto))
295 (map () (curry #'ensure-service trace) (proto-services proto))
296 (loop for type being the hash-keys of *undefined-messages*
297 using (hash-value things)
299 (mapcar #'(lambda (thing)
300 (format nil "~A:~A" (proto-name (car thing)) (proto-name (cdr thing))))
301 things)) into warnings
302 finally (return (values warnings *undefined-messages*)))))
304 (defmethod ensure-message (trace (message protobuf-message))
305 (let ((trace (cons message trace)))
306 (map () (curry #'ensure-message trace) (proto-messages message))
307 (map () (curry #'ensure-field trace message) (proto-fields message))))
309 (defmethod ensure-field (trace message (field protobuf-field))
310 (ensure-type trace message field (proto-class field)))
312 (defmethod ensure-service (trace (service protobuf-service))
313 (map () (curry #'ensure-rpc trace service) (proto-rpcs service)))
315 (defmethod ensure-rpc (trace service (rpc protobuf-rpc))
316 (ensure-type trace service rpc (proto-input-type rpc))
317 (ensure-type trace service rpc (proto-output-type rpc)))
319 ;; 'message' and 'field' can be a message and a field or a service and an RPC
320 (defun ensure-type (trace message field type)
321 (unless (keywordp type)
322 (let ((msg (loop for p in trace
323 thereis (or (find-message-for-class p type)
324 (find-enum-for-type p type)))))
326 (push (cons message field) (gethash type *undefined-messages*))))))