bd1c259cb5fd3769621277727ffa2642d70bc097
[sliver-openvswitch.git] / python / ovs / db / types.py
1 # Copyright (c) 2009, 2010, 2011, 2012 Nicira, Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at:
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import sys
16 import uuid
17
18 from ovs.db import error
19 import ovs.db.parser
20 import ovs.db.data
21 import ovs.ovsuuid
22
23
24 class AtomicType(object):
25     def __init__(self, name, default, python_types):
26         self.name = name
27         self.default = default
28         self.python_types = python_types
29
30     @staticmethod
31     def from_string(s):
32         if s != "void":
33             for atomic_type in ATOMIC_TYPES:
34                 if s == atomic_type.name:
35                     return atomic_type
36         raise error.Error('"%s" is not an atomic-type' % s, s)
37
38     @staticmethod
39     def from_json(json):
40         if type(json) not in [str, unicode]:
41             raise error.Error("atomic-type expected", json)
42         else:
43             return AtomicType.from_string(json)
44
45     def __str__(self):
46         return self.name
47
48     def to_string(self):
49         return self.name
50
51     def to_json(self):
52         return self.name
53
54     def default_atom(self):
55         return ovs.db.data.Atom(self, self.default)
56
57 VoidType = AtomicType("void", None, ())
58 IntegerType = AtomicType("integer", 0, (int, long))
59 RealType = AtomicType("real", 0.0, (int, long, float))
60 BooleanType = AtomicType("boolean", False, (bool,))
61 StringType = AtomicType("string", "", (str, unicode))
62 UuidType = AtomicType("uuid", ovs.ovsuuid.zero(), (uuid.UUID,))
63
64 ATOMIC_TYPES = [VoidType, IntegerType, RealType, BooleanType, StringType,
65                 UuidType]
66
67
68 def escapeCString(src):
69     dst = ""
70     for c in src:
71         if c in "\\\"":
72             dst += "\\" + c
73         elif ord(c) < 32:
74             if c == '\n':
75                 dst += '\\n'
76             elif c == '\r':
77                 dst += '\\r'
78             elif c == '\a':
79                 dst += '\\a'
80             elif c == '\b':
81                 dst += '\\b'
82             elif c == '\f':
83                 dst += '\\f'
84             elif c == '\t':
85                 dst += '\\t'
86             elif c == '\v':
87                 dst += '\\v'
88             else:
89                 dst += '\\%03o' % ord(c)
90         else:
91             dst += c
92     return dst
93
94
95 def commafy(x):
96     """Returns integer x formatted in decimal with thousands set off by
97     commas."""
98     return _commafy("%d" % x)
99
100
101 def _commafy(s):
102     if s.startswith('-'):
103         return '-' + _commafy(s[1:])
104     elif len(s) <= 3:
105         return s
106     else:
107         return _commafy(s[:-3]) + ',' + _commafy(s[-3:])
108
109
110 def returnUnchanged(x):
111     return x
112
113
114 class BaseType(object):
115     def __init__(self, type_, enum=None, min=None, max=None,
116                  min_length=0, max_length=sys.maxint, ref_table_name=None):
117         assert isinstance(type_, AtomicType)
118         self.type = type_
119         self.enum = enum
120         self.min = min
121         self.max = max
122         self.min_length = min_length
123         self.max_length = max_length
124         self.ref_table_name = ref_table_name
125         if ref_table_name:
126             self.ref_type = 'strong'
127         else:
128             self.ref_type = None
129         self.ref_table = None
130
131     def default(self):
132         return ovs.db.data.Atom.default(self.type)
133
134     def __eq__(self, other):
135         if not isinstance(other, BaseType):
136             return NotImplemented
137         return (self.type == other.type and self.enum == other.enum and
138                 self.min == other.min and self.max == other.max and
139                 self.min_length == other.min_length and
140                 self.max_length == other.max_length and
141                 self.ref_table_name == other.ref_table_name)
142
143     def __ne__(self, other):
144         if not isinstance(other, BaseType):
145             return NotImplemented
146         else:
147             return not (self == other)
148
149     @staticmethod
150     def __parse_uint(parser, name, default):
151         value = parser.get_optional(name, [int, long])
152         if value is None:
153             value = default
154         else:
155             max_value = 2 ** 32 - 1
156             if not (0 <= value <= max_value):
157                 raise error.Error("%s out of valid range 0 to %d"
158                                   % (name, max_value), value)
159         return value
160
161     @staticmethod
162     def from_json(json):
163         if type(json) in [str, unicode]:
164             return BaseType(AtomicType.from_json(json))
165
166         parser = ovs.db.parser.Parser(json, "ovsdb type")
167         atomic_type = AtomicType.from_json(parser.get("type", [str, unicode]))
168
169         base = BaseType(atomic_type)
170
171         enum = parser.get_optional("enum", [])
172         if enum is not None:
173             base.enum = ovs.db.data.Datum.from_json(
174                     BaseType.get_enum_type(base.type), enum)
175         elif base.type == IntegerType:
176             base.min = parser.get_optional("minInteger", [int, long])
177             base.max = parser.get_optional("maxInteger", [int, long])
178             if (base.min is not None and base.max is not None
179                     and base.min > base.max):
180                 raise error.Error("minInteger exceeds maxInteger", json)
181         elif base.type == RealType:
182             base.min = parser.get_optional("minReal", [int, long, float])
183             base.max = parser.get_optional("maxReal", [int, long, float])
184             if (base.min is not None and base.max is not None
185                     and base.min > base.max):
186                 raise error.Error("minReal exceeds maxReal", json)
187         elif base.type == StringType:
188             base.min_length = BaseType.__parse_uint(parser, "minLength", 0)
189             base.max_length = BaseType.__parse_uint(parser, "maxLength",
190                                                     sys.maxint)
191             if base.min_length > base.max_length:
192                 raise error.Error("minLength exceeds maxLength", json)
193         elif base.type == UuidType:
194             base.ref_table_name = parser.get_optional("refTable", ['id'])
195             if base.ref_table_name:
196                 base.ref_type = parser.get_optional("refType", [str, unicode],
197                                                    "strong")
198                 if base.ref_type not in ['strong', 'weak']:
199                     raise error.Error('refType must be "strong" or "weak" '
200                                       '(not "%s")' % base.ref_type)
201         parser.finish()
202
203         return base
204
205     def to_json(self):
206         if not self.has_constraints():
207             return self.type.to_json()
208
209         json = {'type': self.type.to_json()}
210
211         if self.enum:
212             json['enum'] = self.enum.to_json()
213
214         if self.type == IntegerType:
215             if self.min is not None:
216                 json['minInteger'] = self.min
217             if self.max is not None:
218                 json['maxInteger'] = self.max
219         elif self.type == RealType:
220             if self.min is not None:
221                 json['minReal'] = self.min
222             if self.max is not None:
223                 json['maxReal'] = self.max
224         elif self.type == StringType:
225             if self.min_length != 0:
226                 json['minLength'] = self.min_length
227             if self.max_length != sys.maxint:
228                 json['maxLength'] = self.max_length
229         elif self.type == UuidType:
230             if self.ref_table_name:
231                 json['refTable'] = self.ref_table_name
232                 if self.ref_type != 'strong':
233                     json['refType'] = self.ref_type
234         return json
235
236     def copy(self):
237         base = BaseType(self.type, self.enum.copy(), self.min, self.max,
238                         self.min_length, self.max_length, self.ref_table_name)
239         base.ref_table = self.ref_table
240         return base
241
242     def is_valid(self):
243         if self.type in (VoidType, BooleanType, UuidType):
244             return True
245         elif self.type in (IntegerType, RealType):
246             return self.min is None or self.max is None or self.min <= self.max
247         elif self.type == StringType:
248             return self.min_length <= self.max_length
249         else:
250             return False
251
252     def has_constraints(self):
253         return (self.enum is not None or self.min is not None or
254                 self.max is not None or
255                 self.min_length != 0 or self.max_length != sys.maxint or
256                 self.ref_table_name is not None)
257
258     def without_constraints(self):
259         return BaseType(self.type)
260
261     @staticmethod
262     def get_enum_type(atomic_type):
263         """Returns the type of the 'enum' member for a BaseType whose
264         'type' is 'atomic_type'."""
265         return Type(BaseType(atomic_type), None, 1, sys.maxint)
266
267     def is_ref(self):
268         return self.type == UuidType and self.ref_table_name is not None
269
270     def is_strong_ref(self):
271         return self.is_ref() and self.ref_type == 'strong'
272
273     def is_weak_ref(self):
274         return self.is_ref() and self.ref_type == 'weak'
275
276     def toEnglish(self, escapeLiteral=returnUnchanged):
277         if self.type == UuidType and self.ref_table_name:
278             s = escapeLiteral(self.ref_table_name)
279             if self.ref_type == 'weak':
280                 s = "weak reference to " + s
281             return s
282         else:
283             return self.type.to_string()
284
285     def constraintsToEnglish(self, escapeLiteral=returnUnchanged,
286                              escapeNumber=returnUnchanged):
287         if self.enum:
288             literals = [value.toEnglish(escapeLiteral)
289                         for value in self.enum.values]
290             if len(literals) == 1:
291                 english = 'must be %s' % (literals[0])
292             elif len(literals) == 2:
293                 english = 'either %s or %s' % (literals[0], literals[1])
294             else:
295                 english = 'one of %s, %s, or %s' % (literals[0],
296                                                     ', '.join(literals[1:-1]),
297                                                     literals[-1])
298         elif self.min is not None and self.max is not None:
299             if self.type == IntegerType:
300                 english = 'in range %s to %s' % (
301                     escapeNumber(commafy(self.min)),
302                     escapeNumber(commafy(self.max)))
303             else:
304                 english = 'in range %s to %s' % (
305                     escapeNumber("%g" % self.min),
306                     escapeNumber("%g" % self.max))
307         elif self.min is not None:
308             if self.type == IntegerType:
309                 english = 'at least %s' % escapeNumber(commafy(self.min))
310             else:
311                 english = 'at least %s' % escapeNumber("%g" % self.min)
312         elif self.max is not None:
313             if self.type == IntegerType:
314                 english = 'at most %s' % escapeNumber(commafy(self.max))
315             else:
316                 english = 'at most %s' % escapeNumber("%g" % self.max)
317         elif self.min_length != 0 and self.max_length != sys.maxint:
318             if self.min_length == self.max_length:
319                 english = ('exactly %s characters long'
320                            % commafy(self.min_length))
321             else:
322                 english = ('between %s and %s characters long'
323                         % (commafy(self.min_length),
324                            commafy(self.max_length)))
325         elif self.min_length != 0:
326             return 'at least %s characters long' % commafy(self.min_length)
327         elif self.max_length != sys.maxint:
328             english = 'at most %s characters long' % commafy(self.max_length)
329         else:
330             english = ''
331
332         return english
333
334     def toCType(self, prefix):
335         if self.ref_table_name:
336             return "struct %s%s *" % (prefix, self.ref_table_name.lower())
337         else:
338             return {IntegerType: 'int64_t ',
339                     RealType: 'double ',
340                     UuidType: 'struct uuid ',
341                     BooleanType: 'bool ',
342                     StringType: 'char *'}[self.type]
343
344     def toAtomicType(self):
345         return "OVSDB_TYPE_%s" % self.type.to_string().upper()
346
347     def copyCValue(self, dst, src):
348         args = {'dst': dst, 'src': src}
349         if self.ref_table_name:
350             return ("%(dst)s = %(src)s->header_.uuid;") % args
351         elif self.type == StringType:
352             return "%(dst)s = xstrdup(%(src)s);" % args
353         else:
354             return "%(dst)s = %(src)s;" % args
355
356     def initCDefault(self, var, is_optional):
357         if self.ref_table_name:
358             return "%s = NULL;" % var
359         elif self.type == StringType and not is_optional:
360             return '%s = "";' % var
361         else:
362             pattern = {IntegerType: '%s = 0;',
363                        RealType: '%s = 0.0;',
364                        UuidType: 'uuid_zero(&%s);',
365                        BooleanType: '%s = false;',
366                        StringType: '%s = NULL;'}[self.type]
367             return pattern % var
368
369     def cInitBaseType(self, indent, var):
370         stmts = []
371         stmts.append('ovsdb_base_type_init(&%s, %s);' % (
372                 var, self.toAtomicType()))
373         if self.enum:
374             stmts.append("%s.enum_ = xmalloc(sizeof *%s.enum_);"
375                          % (var, var))
376             stmts += self.enum.cInitDatum("%s.enum_" % var)
377         if self.type == IntegerType:
378             if self.min is not None:
379                 stmts.append('%s.u.integer.min = INT64_C(%d);'
380                         % (var, self.min))
381             if self.max is not None:
382                 stmts.append('%s.u.integer.max = INT64_C(%d);'
383                         % (var, self.max))
384         elif self.type == RealType:
385             if self.min is not None:
386                 stmts.append('%s.u.real.min = %d;' % (var, self.min))
387             if self.max is not None:
388                 stmts.append('%s.u.real.max = %d;' % (var, self.max))
389         elif self.type == StringType:
390             if self.min_length is not None:
391                 stmts.append('%s.u.string.minLen = %d;'
392                         % (var, self.min_length))
393             if self.max_length != sys.maxint:
394                 stmts.append('%s.u.string.maxLen = %d;'
395                         % (var, self.max_length))
396         elif self.type == UuidType:
397             if self.ref_table_name is not None:
398                 stmts.append('%s.u.uuid.refTableName = "%s";'
399                         % (var, escapeCString(self.ref_table_name)))
400                 stmts.append('%s.u.uuid.refType = OVSDB_REF_%s;'
401                         % (var, self.ref_type.upper()))
402         return '\n'.join([indent + stmt for stmt in stmts])
403
404
405 class Type(object):
406     DEFAULT_MIN = 1
407     DEFAULT_MAX = 1
408
409     def __init__(self, key, value=None, n_min=DEFAULT_MIN, n_max=DEFAULT_MAX):
410         self.key = key
411         self.value = value
412         self.n_min = n_min
413         self.n_max = n_max
414
415     def copy(self):
416         if self.value is None:
417             value = None
418         else:
419             value = self.value.copy()
420         return Type(self.key.copy(), value, self.n_min, self.n_max)
421
422     def __eq__(self, other):
423         if not isinstance(other, Type):
424             return NotImplemented
425         return (self.key == other.key and self.value == other.value and
426                 self.n_min == other.n_min and self.n_max == other.n_max)
427
428     def __ne__(self, other):
429         if not isinstance(other, Type):
430             return NotImplemented
431         else:
432             return not (self == other)
433
434     def is_valid(self):
435         return (self.key.type != VoidType and self.key.is_valid() and
436                 (self.value is None or
437                  (self.value.type != VoidType and self.value.is_valid())) and
438                 self.n_min <= 1 <= self.n_max)
439
440     def is_scalar(self):
441         return self.n_min == 1 and self.n_max == 1 and not self.value
442
443     def is_optional(self):
444         return self.n_min == 0 and self.n_max == 1
445
446     def is_composite(self):
447         return self.n_max > 1
448
449     def is_set(self):
450         return self.value is None and (self.n_min != 1 or self.n_max != 1)
451
452     def is_map(self):
453         return self.value is not None
454
455     def is_smap(self):
456         return (self.is_map()
457                 and self.key.type == StringType
458                 and self.value.type == StringType)
459
460     def is_optional_pointer(self):
461         return (self.is_optional() and not self.value
462                 and (self.key.type == StringType or self.key.ref_table_name))
463
464     @staticmethod
465     def __n_from_json(json, default):
466         if json is None:
467             return default
468         elif type(json) == int and 0 <= json <= sys.maxint:
469             return json
470         else:
471             raise error.Error("bad min or max value", json)
472
473     @staticmethod
474     def from_json(json):
475         if type(json) in [str, unicode]:
476             return Type(BaseType.from_json(json))
477
478         parser = ovs.db.parser.Parser(json, "ovsdb type")
479         key_json = parser.get("key", [dict, str, unicode])
480         value_json = parser.get_optional("value", [dict, str, unicode])
481         min_json = parser.get_optional("min", [int])
482         max_json = parser.get_optional("max", [int, str, unicode])
483         parser.finish()
484
485         key = BaseType.from_json(key_json)
486         if value_json:
487             value = BaseType.from_json(value_json)
488         else:
489             value = None
490
491         n_min = Type.__n_from_json(min_json, Type.DEFAULT_MIN)
492
493         if max_json == 'unlimited':
494             n_max = sys.maxint
495         else:
496             n_max = Type.__n_from_json(max_json, Type.DEFAULT_MAX)
497
498         type_ = Type(key, value, n_min, n_max)
499         if not type_.is_valid():
500             raise error.Error("ovsdb type fails constraint checks", json)
501         return type_
502
503     def to_json(self):
504         if self.is_scalar() and not self.key.has_constraints():
505             return self.key.to_json()
506
507         json = {"key": self.key.to_json()}
508         if self.value is not None:
509             json["value"] = self.value.to_json()
510         if self.n_min != Type.DEFAULT_MIN:
511             json["min"] = self.n_min
512         if self.n_max == sys.maxint:
513             json["max"] = "unlimited"
514         elif self.n_max != Type.DEFAULT_MAX:
515             json["max"] = self.n_max
516         return json
517
518     def toEnglish(self, escapeLiteral=returnUnchanged):
519         keyName = self.key.toEnglish(escapeLiteral)
520         if self.value:
521             valueName = self.value.toEnglish(escapeLiteral)
522
523         if self.is_scalar():
524             return keyName
525         elif self.is_optional():
526             if self.value:
527                 return "optional %s-%s pair" % (keyName, valueName)
528             else:
529                 return "optional %s" % keyName
530         else:
531             if self.n_max == sys.maxint:
532                 if self.n_min:
533                     quantity = "%s or more " % commafy(self.n_min)
534                 else:
535                     quantity = ""
536             elif self.n_min:
537                 quantity = "%s to %s " % (commafy(self.n_min),
538                                           commafy(self.n_max))
539             else:
540                 quantity = "up to %s " % commafy(self.n_max)
541
542             if self.value:
543                 return "map of %s%s-%s pairs" % (quantity, keyName, valueName)
544             else:
545                 if keyName.endswith('s'):
546                     plural = keyName + "es"
547                 else:
548                     plural = keyName + "s"
549                 return "set of %s%s" % (quantity, plural)
550
551     def constraintsToEnglish(self, escapeLiteral=returnUnchanged,
552                              escapeNumber=returnUnchanged):
553         constraints = []
554         keyConstraints = self.key.constraintsToEnglish(escapeLiteral,
555                                                        escapeNumber)
556         if keyConstraints:
557             if self.value:
558                 constraints.append('key %s' % keyConstraints)
559             else:
560                 constraints.append(keyConstraints)
561
562         if self.value:
563             valueConstraints = self.value.constraintsToEnglish(escapeLiteral,
564                                                                escapeNumber)
565             if valueConstraints:
566                 constraints.append('value %s' % valueConstraints)
567
568         return ', '.join(constraints)
569
570     def cDeclComment(self):
571         if self.n_min == 1 and self.n_max == 1 and self.key.type == StringType:
572             return "\t/* Always nonnull. */"
573         else:
574             return ""
575
576     def cInitType(self, indent, var):
577         initKey = self.key.cInitBaseType(indent, "%s.key" % var)
578         if self.value:
579             initValue = self.value.cInitBaseType(indent, "%s.value" % var)
580         else:
581             initValue = ('%sovsdb_base_type_init(&%s.value, '
582                          'OVSDB_TYPE_VOID);' % (indent, var))
583         initMin = "%s%s.n_min = %s;" % (indent, var, self.n_min)
584         if self.n_max == sys.maxint:
585             n_max = "UINT_MAX"
586         else:
587             n_max = self.n_max
588         initMax = "%s%s.n_max = %s;" % (indent, var, n_max)
589         return "\n".join((initKey, initValue, initMin, initMax))