App Engine Python SDK version 1.9.3
[gae.git] / python / lib / endpoints-1.0 / endpoints / message_parser.py
blob256230d2672870a6d4d38408a7e27531c849fdf3
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
19 """Describe ProtoRPC Messages in JSON Schema.
21 Add protorpc.message subclasses to MessageTypeToJsonSchema and get a JSON
22 Schema description of all the messages.
23 """
26 import re
28 from protorpc import message_types
29 from protorpc import messages
31 __all__ = ['MessageTypeToJsonSchema']
34 class MessageTypeToJsonSchema(object):
35 """Describe ProtoRPC messages in JSON Schema.
37 Add protorpc.message subclasses to MessageTypeToJsonSchema and get a JSON
38 Schema description of all the messages. MessageTypeToJsonSchema handles
39 all the types of fields that can appear in a message.
40 """
48 __FIELD_TO_SCHEMA_TYPE_MAP = {
49 messages.IntegerField: {messages.Variant.INT32: ('integer', 'int32'),
50 messages.Variant.INT64: ('string', 'int64'),
51 messages.Variant.UINT32: ('integer', 'uint32'),
52 messages.Variant.UINT64: ('string', 'uint64'),
53 messages.Variant.SINT32: ('integer', 'int32'),
54 messages.Variant.SINT64: ('string', 'int64'),
55 None: ('integer', 'int64')},
56 messages.FloatField: {messages.Variant.FLOAT: ('number', 'float'),
57 messages.Variant.DOUBLE: ('number', 'double'),
58 None: ('number', 'float')},
59 messages.BooleanField: ('boolean', None),
60 messages.BytesField: ('string', 'byte'),
61 message_types.DateTimeField: ('string', 'date-time'),
62 messages.StringField: ('string', None),
63 messages.MessageField: ('object', None),
64 messages.EnumField: ('string', None),
67 __DEFAULT_SCHEMA_TYPE = ('string', None)
69 def __init__(self):
71 self.__schemas = {}
74 self.__normalized_names = {}
76 def add_message(self, message_type):
77 """Add a new message.
79 Args:
80 message_type: protorpc.message.Message class to be parsed.
82 Returns:
83 string, The JSON Schema id.
85 Raises:
86 KeyError if the Schema id for this message_type would collide with the
87 Schema id of a different message_type that was already added.
88 """
89 name = self.__normalized_name(message_type)
90 if name not in self.__schemas:
92 self.__schemas[name] = None
93 schema = self.__message_to_schema(message_type)
94 self.__schemas[name] = schema
95 return name
97 def ref_for_message_type(self, message_type):
98 """Returns the JSON Schema id for the given message.
100 Args:
101 message_type: protorpc.message.Message class to be parsed.
103 Returns:
104 string, The JSON Schema id.
106 Raises:
107 KeyError: if the message hasn't been parsed via add_message().
109 name = self.__normalized_name(message_type)
110 if name not in self.__schemas:
111 raise KeyError('Message has not been parsed: %s', name)
112 return name
114 def schemas(self):
115 """Returns the JSON Schema of all the messages.
117 Returns:
118 object: JSON Schema description of all messages.
120 return self.__schemas.copy()
122 def __normalized_name(self, message_type):
123 """Normalized schema name.
125 Generate a normalized schema name, taking the class name and stripping out
126 everything but alphanumerics, and camel casing the remaining words.
127 A normalized schema name is a name that matches [a-zA-Z][a-zA-Z0-9]*
129 Args:
130 message_type: protorpc.message.Message class being parsed.
132 Returns:
133 A string, the normalized schema name.
135 Raises:
136 KeyError if a collision is found between normalized names.
140 name = message_type.definition_name()
142 split_name = re.split(r'[^0-9a-zA-Z]', name)
143 normalized = ''.join(
144 part[0].upper() + part[1:] for part in split_name if part)
146 previous = self.__normalized_names.get(normalized)
147 if previous:
148 if previous != name:
149 raise KeyError('Both %s and %s normalize to the same schema name: %s' %
150 (name, previous, normalized))
151 else:
152 self.__normalized_names[normalized] = name
154 return normalized
156 def __message_to_schema(self, message_type):
157 """Parse a single message into JSON Schema.
159 Will recursively descend the message structure
160 and also parse other messages references via MessageFields.
162 Args:
163 message_type: protorpc.messages.Message class to parse.
165 Returns:
166 An object representation of the schema.
168 name = self.__normalized_name(message_type)
169 schema = {
170 'id': name,
171 'type': 'object',
173 if message_type.__doc__:
174 schema['description'] = message_type.__doc__
175 properties = {}
176 for field in message_type.all_fields():
177 descriptor = {}
181 type_info = {}
183 if type(field) == messages.MessageField:
184 field_type = field.type().__class__
185 type_info['$ref'] = self.add_message(field_type)
186 if field_type.__doc__:
187 descriptor['description'] = field_type.__doc__
188 else:
189 schema_type = self.__FIELD_TO_SCHEMA_TYPE_MAP.get(
190 type(field), self.__DEFAULT_SCHEMA_TYPE)
193 if isinstance(schema_type, dict):
194 variant_map = schema_type
195 variant = getattr(field, 'variant', None)
196 if variant in variant_map:
197 schema_type = variant_map[variant]
198 else:
200 schema_type = variant_map[None]
201 type_info['type'] = schema_type[0]
202 if schema_type[1]:
203 type_info['format'] = schema_type[1]
205 if type(field) == messages.EnumField:
206 sorted_enums = sorted([enum_info for enum_info in field.type],
207 key=lambda enum_info: enum_info.number)
208 type_info['enum'] = [enum_info.name for enum_info in sorted_enums]
210 if field.required:
211 descriptor['required'] = True
213 if field.default:
214 if type(field) == messages.EnumField:
215 descriptor['default'] = str(field.default)
216 else:
217 descriptor['default'] = field.default
219 if field.repeated:
220 descriptor['items'] = type_info
221 descriptor['type'] = 'array'
222 else:
223 descriptor.update(type_info)
225 properties[field.name] = descriptor
227 schema['properties'] = properties
229 return schema