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