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