18b7081ecbd51a9fc2866ac2d4302a36eca477ca
[openvswitch] / python / ovs / db / types.py
1 # Copyright (c) 2009, 2010, 2011 Nicira Networks
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         if self.enum:
287             literals = [value.toEnglish(escapeLiteral)
288                         for value in self.enum.values]
289             if len(literals) == 2:
290                 english = 'either %s or %s' % (literals[0], literals[1])
291             else:
292                 english = 'one of %s, %s, or %s' % (literals[0],
293                                                     ', '.join(literals[1:-1]),
294                                                     literals[-1])
295         elif self.min is not None and self.max is not None:
296             if self.type == IntegerType:
297                 english = 'in range %s to %s' % (commafy(self.min),
298                                                  commafy(self.max))
299             else:
300                 english = 'in range %g to %g' % (self.min, self.max)
301         elif self.min is not None:
302             if self.type == IntegerType:
303                 english = 'at least %s' % commafy(self.min)
304             else:
305                 english = 'at least %g' % self.min
306         elif self.max is not None:
307             if self.type == IntegerType:
308                 english = 'at most %s' % commafy(self.max)
309             else:
310                 english = 'at most %g' % self.max
311         elif self.min_length != 0 and self.max_length != sys.maxint:
312             if self.min_length == self.max_length:
313                 english = 'exactly %d characters long' % (self.min_length)
314             else:
315                 english = ('between %d and %d characters long'
316                         % (self.min_length, self.max_length))
317         elif self.min_length != 0:
318             return 'at least %d characters long' % self.min_length
319         elif self.max_length != sys.maxint:
320             english = 'at most %d characters long' % self.max_length
321         else:
322             english = ''
323
324         return english
325
326     def toCType(self, prefix):
327         if self.ref_table_name:
328             return "struct %s%s *" % (prefix, self.ref_table_name.lower())
329         else:
330             return {IntegerType: 'int64_t ',
331                     RealType: 'double ',
332                     UuidType: 'struct uuid ',
333                     BooleanType: 'bool ',
334                     StringType: 'char *'}[self.type]
335
336     def toAtomicType(self):
337         return "OVSDB_TYPE_%s" % self.type.to_string().upper()
338
339     def copyCValue(self, dst, src):
340         args = {'dst': dst, 'src': src}
341         if self.ref_table_name:
342             return ("%(dst)s = %(src)s->header_.uuid;") % args
343         elif self.type == StringType:
344             return "%(dst)s = xstrdup(%(src)s);" % args
345         else:
346             return "%(dst)s = %(src)s;" % args
347
348     def initCDefault(self, var, is_optional):
349         if self.ref_table_name:
350             return "%s = NULL;" % var
351         elif self.type == StringType and not is_optional:
352             return '%s = "";' % var
353         else:
354             pattern = {IntegerType: '%s = 0;',
355                        RealType: '%s = 0.0;',
356                        UuidType: 'uuid_zero(&%s);',
357                        BooleanType: '%s = false;',
358                        StringType: '%s = NULL;'}[self.type]
359             return pattern % var
360
361     def cInitBaseType(self, indent, var):
362         stmts = []
363         stmts.append('ovsdb_base_type_init(&%s, %s);' % (
364                 var, self.toAtomicType()))
365         if self.enum:
366             stmts.append("%s.enum_ = xmalloc(sizeof *%s.enum_);"
367                          % (var, var))
368             stmts += self.enum.cInitDatum("%s.enum_" % var)
369         if self.type == IntegerType:
370             if self.min is not None:
371                 stmts.append('%s.u.integer.min = INT64_C(%d);'
372                         % (var, self.min))
373             if self.max is not None:
374                 stmts.append('%s.u.integer.max = INT64_C(%d);'
375                         % (var, self.max))
376         elif self.type == RealType:
377             if self.min is not None:
378                 stmts.append('%s.u.real.min = %d;' % (var, self.min))
379             if self.max is not None:
380                 stmts.append('%s.u.real.max = %d;' % (var, self.max))
381         elif self.type == StringType:
382             if self.min_length is not None:
383                 stmts.append('%s.u.string.minLen = %d;'
384                         % (var, self.min_length))
385             if self.max_length != sys.maxint:
386                 stmts.append('%s.u.string.maxLen = %d;'
387                         % (var, self.max_length))
388         elif self.type == UuidType:
389             if self.ref_table_name is not None:
390                 stmts.append('%s.u.uuid.refTableName = "%s";'
391                         % (var, escapeCString(self.ref_table_name)))
392                 stmts.append('%s.u.uuid.refType = OVSDB_REF_%s;'
393                         % (var, self.ref_type.upper()))
394         return '\n'.join([indent + stmt for stmt in stmts])
395
396
397 class Type(object):
398     DEFAULT_MIN = 1
399     DEFAULT_MAX = 1
400
401     def __init__(self, key, value=None, n_min=DEFAULT_MIN, n_max=DEFAULT_MAX):
402         self.key = key
403         self.value = value
404         self.n_min = n_min
405         self.n_max = n_max
406
407     def copy(self):
408         if self.value is None:
409             value = None
410         else:
411             value = self.value.copy()
412         return Type(self.key.copy(), value, self.n_min, self.n_max)
413
414     def __eq__(self, other):
415         if not isinstance(other, Type):
416             return NotImplemented
417         return (self.key == other.key and self.value == other.value and
418                 self.n_min == other.n_min and self.n_max == other.n_max)
419
420     def __ne__(self, other):
421         if not isinstance(other, Type):
422             return NotImplemented
423         else:
424             return not (self == other)
425
426     def is_valid(self):
427         return (self.key.type != VoidType and self.key.is_valid() and
428                 (self.value is None or
429                  (self.value.type != VoidType and self.value.is_valid())) and
430                 self.n_min <= 1 <= self.n_max)
431
432     def is_scalar(self):
433         return self.n_min == 1 and self.n_max == 1 and not self.value
434
435     def is_optional(self):
436         return self.n_min == 0 and self.n_max == 1
437
438     def is_composite(self):
439         return self.n_max > 1
440
441     def is_set(self):
442         return self.value is None and (self.n_min != 1 or self.n_max != 1)
443
444     def is_map(self):
445         return self.value is not None
446
447     def is_optional_pointer(self):
448         return (self.is_optional() and not self.value
449                 and (self.key.type == StringType or self.key.ref_table_name))
450
451     @staticmethod
452     def __n_from_json(json, default):
453         if json is None:
454             return default
455         elif type(json) == int and 0 <= json <= sys.maxint:
456             return json
457         else:
458             raise error.Error("bad min or max value", json)
459
460     @staticmethod
461     def from_json(json):
462         if type(json) in [str, unicode]:
463             return Type(BaseType.from_json(json))
464
465         parser = ovs.db.parser.Parser(json, "ovsdb type")
466         key_json = parser.get("key", [dict, str, unicode])
467         value_json = parser.get_optional("value", [dict, str, unicode])
468         min_json = parser.get_optional("min", [int])
469         max_json = parser.get_optional("max", [int, str, unicode])
470         parser.finish()
471
472         key = BaseType.from_json(key_json)
473         if value_json:
474             value = BaseType.from_json(value_json)
475         else:
476             value = None
477
478         n_min = Type.__n_from_json(min_json, Type.DEFAULT_MIN)
479
480         if max_json == 'unlimited':
481             n_max = sys.maxint
482         else:
483             n_max = Type.__n_from_json(max_json, Type.DEFAULT_MAX)
484
485         type_ = Type(key, value, n_min, n_max)
486         if not type_.is_valid():
487             raise error.Error("ovsdb type fails constraint checks", json)
488         return type_
489
490     def to_json(self):
491         if self.is_scalar() and not self.key.has_constraints():
492             return self.key.to_json()
493
494         json = {"key": self.key.to_json()}
495         if self.value is not None:
496             json["value"] = self.value.to_json()
497         if self.n_min != Type.DEFAULT_MIN:
498             json["min"] = self.n_min
499         if self.n_max == sys.maxint:
500             json["max"] = "unlimited"
501         elif self.n_max != Type.DEFAULT_MAX:
502             json["max"] = self.n_max
503         return json
504
505     def toEnglish(self, escapeLiteral=returnUnchanged):
506         keyName = self.key.toEnglish(escapeLiteral)
507         if self.value:
508             valueName = self.value.toEnglish(escapeLiteral)
509
510         if self.is_scalar():
511             return keyName
512         elif self.is_optional():
513             if self.value:
514                 return "optional %s-%s pair" % (keyName, valueName)
515             else:
516                 return "optional %s" % keyName
517         else:
518             if self.n_max == sys.maxint:
519                 if self.n_min:
520                     quantity = "%d or more " % self.n_min
521                 else:
522                     quantity = ""
523             elif self.n_min:
524                 quantity = "%d to %d " % (self.n_min, self.n_max)
525             else:
526                 quantity = "up to %d " % self.n_max
527
528             if self.value:
529                 return "map of %s%s-%s pairs" % (quantity, keyName, valueName)
530             else:
531                 if keyName.endswith('s'):
532                     plural = keyName + "es"
533                 else:
534                     plural = keyName + "s"
535                 return "set of %s%s" % (quantity, plural)
536
537     def constraintsToEnglish(self, escapeLiteral=returnUnchanged):
538         constraints = []
539         keyConstraints = self.key.constraintsToEnglish(escapeLiteral)
540         if keyConstraints:
541             if self.value:
542                 constraints.append('key %s' % keyConstraints)
543             else:
544                 constraints.append(keyConstraints)
545
546         if self.value:
547             valueConstraints = self.value.constraintsToEnglish(escapeLiteral)
548             if valueConstraints:
549                 constraints.append('value %s' % valueConstraints)
550
551         return ', '.join(constraints)
552
553     def cDeclComment(self):
554         if self.n_min == 1 and self.n_max == 1 and self.key.type == StringType:
555             return "\t/* Always nonnull. */"
556         else:
557             return ""
558
559     def cInitType(self, indent, var):
560         initKey = self.key.cInitBaseType(indent, "%s.key" % var)
561         if self.value:
562             initValue = self.value.cInitBaseType(indent, "%s.value" % var)
563         else:
564             initValue = ('%sovsdb_base_type_init(&%s.value, '
565                          'OVSDB_TYPE_VOID);' % (indent, var))
566         initMin = "%s%s.n_min = %s;" % (indent, var, self.n_min)
567         if self.n_max == sys.maxint:
568             n_max = "UINT_MAX"
569         else:
570             n_max = self.n_max
571         initMax = "%s%s.n_max = %s;" % (indent, var, n_max)
572         return "\n".join((initKey, initValue, initMin, initMax))