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 generation from ordinary CLOS classes
16 ;; Doing this can't really work perfectly, there's not enough information
17 ;; - How do we decide if there's an ownership hierarchy that should produce embedded messages?
18 ;; - How do we decide if there are volatile slots that should not be included in the message?
19 (defun write-protobuf-schema-for-classes (classes
20 &key (stream *standard-output*) (type :proto) proto-name
21 package slot-filter type-filter enum-filter value-filter)
22 "Given a set of CLOS classes, generates a Protobufs schema for the classes
23 and pretty prints the schema to the stream.
24 The return value is the schema."
25 (let* ((messages (mapcar #'(lambda (c)
26 (class-to-protobuf-message c :slot-filter slot-filter
27 :type-filter type-filter
28 :enum-filter enum-filter
29 :value-filter value-filter))
31 (protobuf (make-instance 'protobuf
33 :package (and package (if (stringp package) package (string-downcase (string package))))
37 (write-protobuf protobuf :stream stream :type type)
42 (defun class-to-protobuf-message (class
43 &key slot-filter type-filter enum-filter value-filter)
44 (let* ((class (find-class class))
45 (slots (class-slots class)))
46 (with-collectors ((enums collect-enum)
48 (fields collect-field))
51 (multiple-value-bind (field msg enum)
52 (slot-to-protobuf-field class s index slots
53 :slot-filter slot-filter
54 :type-filter type-filter
55 :enum-filter enum-filter
56 :value-filter value-filter)
62 (incf index 1) ;don't worry about the 19000-19999 restriction
63 (collect-field field))))
64 (make-instance 'protobuf-message
65 :name (class-name->proto (class-name class))
66 :class (class-name class)
67 :enums (delete-duplicates enums :key #'proto-name :test #'string-equal)
68 :messages (delete-duplicates msgs :key #'proto-name :test #'string-equal)
71 ;; Returns a field, (optionally) an inner message, and (optionally) an inner enum
72 (defun slot-to-protobuf-field (class slot index slots
73 &key slot-filter type-filter enum-filter value-filter)
74 (when (or (null slot-filter)
75 (funcall slot-filter slot slots))
76 (multiple-value-bind (type pclass packed enums)
77 (clos-type-to-protobuf-type (find-slot-definition-type class slot) type-filter enum-filter)
78 (let* ((ename (and enums
79 (format nil "~A-~A" 'enum (slot-definition-name slot))))
81 (let* ((names (mapcar #'enum-name->proto enums))
82 (prefix (and (> (length names) 1)
84 0 (mismatch (first names) (second names))))))
85 (when (and prefix (> (length prefix) 2)
86 (every #'(lambda (name) (starts-with name prefix)) names))
87 (setq names (mapcar #'(lambda (name) (subseq name (length prefix))) names)))
88 (make-instance 'protobuf-enum
89 :name (class-name->proto ename)
90 :class (intern ename (symbol-package (slot-definition-name slot)))
91 :values (loop for name in names
94 collect (make-instance 'protobuf-enum-value
98 (reqd (clos-type-to-protobuf-required (find-slot-definition-type class slot) type-filter))
99 (field (make-instance 'protobuf-field
100 :name (slot-name->proto (slot-definition-name slot))
101 :type (if enum (class-name->proto ename) type)
102 :class (if enum (intern ename (symbol-package (slot-definition-name slot))) pclass)
105 :value (slot-definition-name slot)
106 :reader (find-slot-definition-reader class slot)
107 :default (clos-init-to-protobuf-default (slot-definition-initform slot) value-filter)
109 (values field nil enum)))))
111 ;; Given a class and a slot descriptor, find the unexpanded type definition for the slot
112 (defun find-slot-definition-type (class slotd)
113 (let* ((slot-name (slot-definition-name slotd))
114 (direct-slotd (some #'(lambda (c)
115 (find slot-name (class-direct-slots c) :key #'slot-definition-name))
116 (class-precedence-list class))))
117 (or (and direct-slotd (slot-definition-type slotd))
118 (slot-definition-type slotd))))
120 ;; Given a class and a slot descriptor, find the name of a reader method for the slot
121 (defun find-slot-definition-reader (class slotd)
122 (let* ((slot-name (slot-definition-name slotd))
123 (direct-slotd (some #'(lambda (c)
124 (find slot-name (class-direct-slots c) :key #'slot-definition-name))
125 (class-precedence-list class))))
126 (and direct-slotd (first (slot-definition-readers direct-slotd)))))
128 ;; Returns Protobuf type, a class or primitive type, whether or not to pack the field,
129 ;; and (optionally) a set of enum values
130 (defun clos-type-to-protobuf-type (type &optional type-filter enum-filter)
131 (let ((type (if type-filter (funcall type-filter type) type)))
132 (flet ((type->protobuf-type (type)
135 (values "bool" :bool))
137 (values "int64" :int64))
139 (values "float" :float))
141 (values "double" :double))
143 (values "string" :symbol))
146 (subtypep type '(or string character)))
147 (values "string" :string)
148 (values (class-name->proto type) type))))))
150 (destructuring-bind (head &rest tail) type
153 (when (or (> (length tail) 2)
154 (not (member 'null tail)))
155 (protobufs-warn "Can't handle the complicated OR type ~S" type))
156 (if (eq (first tail) 'null)
157 (clos-type-to-protobuf-type (second tail))
158 (clos-type-to-protobuf-type (first tail))))
160 (if (subtypep type '(list-of t)) ;special knowledge of Quux list-of
161 (let ((satisfies (find 'satisfies tail :key #'car)))
162 (let* ((pred (second satisfies))
163 (type (if (starts-with (string pred) "LIST-OF-")
164 (intern (subseq (string pred) #.(length "LIST-OF-")) (symbol-package pred))
166 (multiple-value-bind (type class)
167 (type->protobuf-type type)
168 (values type class (packed-type-p class)))))
169 (let ((new-tail (remove-if #'(lambda (x) (and (listp x) (eq (car x) 'satisfies))) tail)))
170 (assert (= (length new-tail) 1) ()
171 "Can't handle the complicated AND type ~S" type)
172 (type->protobuf-type (first tail)))))
173 ((member) ;maybe generate an enum type
174 (if (or (equal type '(member t nil))
175 (equal type '(member nil t)))
176 (values "bool" :bool)
177 (let ((values (if enum-filter (funcall enum-filter tail) tail)))
178 (cond ((every #'(lambda (x)
179 (or (null x) (characterp x) (stringp x))) values)
180 (values "string" :string))
181 ((every #'(lambda (x)
182 (or (null x) (and (integerp x) (>= x 0)))) values)
183 (values "uint32" :uint32))
184 ((every #'(lambda (x)
185 (or (null x) (integerp x))) values)
186 (values "int32" :int32))
188 (protobufs-warn "Use DEFTYPE to define a MEMBER type instead of directly using ~S" type)
189 (let ((values (remove-if #'null values)))
190 (values (class-name->proto type)
192 nil ;don't pack enums
193 (if enum-filter (funcall enum-filter values) values))))))))
194 ((list-of) ;special knowledge of Quux list-of
195 (multiple-value-bind (type class)
196 (type->protobuf-type (first tail))
197 (values type class (packed-type-p class))))
199 (let ((lo (or (first tail) '*))
200 (hi (or (second tail) '*)))
201 (if (or (eq lo '*) (< lo 0))
203 (values "int64" :int64)
204 (if (<= (integer-length hi) 32)
205 (values "int32" :int32)
206 (values "int64" :int64)))
208 (values "uint64" :uint64)
209 (if (<= (integer-length hi) 32)
210 (values "uint32" :uint32)
211 (values "uint64" :uint64))))))
213 (let ((len (first tail)))
215 (values "int32" :int32)
216 (values "int64" :int64))))
218 (let ((len (first tail)))
220 (values "uint32" :uint32)
221 (values "uint64" :uint64))))
222 ((float double-float)
223 (type->protobuf-type head))
225 (if (subtypep head '(or string character))
226 (values "string" :string)
227 (error "Don't know how to translate the type ~S" head)))))
228 (type->protobuf-type type)))))
230 (defun packed-type-p (class)
231 (not (null (member class '(:int32 :int64 :uint32 :uint64 :sint32 :sint64
232 :fixed32 :fixed64 :sfixed32 :sfixed64
235 (defun clos-type-to-protobuf-required (type &optional type-filter)
236 (let ((type (if type-filter (funcall type-filter type) type)))
238 (destructuring-bind (head &rest tail) type
241 (let ((optional (member 'null (cdr type))))
242 (if (loop for r in tail
243 thereis (eq (clos-type-to-protobuf-required r) :repeated))
245 (if optional :optional :required))))
247 (if (subtypep type '(list-of t)) ;special knowledge of Quux list-of
251 (if (or (equal type '(member t nil))
252 (equal type '(member nil t)))
254 (if (member nil tail) :optional :required)))
261 (defun clos-init-to-protobuf-default (value &optional value-filter)
262 (let ((value (if value-filter (funcall value-filter value) value)))
263 (and value (constantp value)
264 (format nil "~A" value))))
266 (defun protobuf-default-to-clos-init (default type)
267 (cond ((or (null default)
268 (and (stringp default) (string-empty-p default)))
270 ((member type '(:int32 :uint32 :int64 :uint64 :sint32 :sint64
271 :fixed32 :sfixed32 :fixed64 :sfixed64
273 (read-from-string default))
275 (if (string= default "true") t nil))