python: add set type for ovs.idl.data.Datum.from_python
[cascardo/ovs.git] / python / ovs / db / data.py
1 # Copyright (c) 2009, 2010, 2011, 2014 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 functools
16 import re
17 import uuid
18
19 import six
20
21 import ovs.poller
22 import ovs.socket_util
23 import ovs.json
24 import ovs.jsonrpc
25 import ovs.ovsuuid
26
27 import ovs.db.parser
28 from ovs.db import error
29 import ovs.db.types
30
31
32 class ConstraintViolation(error.Error):
33     def __init__(self, msg, json=None):
34         error.Error.__init__(self, msg, json, tag="constraint violation")
35
36
37 def escapeCString(src):
38     dst = []
39     for c in src:
40         if c in "\\\"":
41             dst.append("\\" + c)
42         elif ord(c) < 32:
43             if c == '\n':
44                 dst.append('\\n')
45             elif c == '\r':
46                 dst.append('\\r')
47             elif c == '\a':
48                 dst.append('\\a')
49             elif c == '\b':
50                 dst.append('\\b')
51             elif c == '\f':
52                 dst.append('\\f')
53             elif c == '\t':
54                 dst.append('\\t')
55             elif c == '\v':
56                 dst.append('\\v')
57             else:
58                 dst.append('\\%03o' % ord(c))
59         else:
60             dst.append(c)
61     return ''.join(dst)
62
63
64 def returnUnchanged(x):
65     return x
66
67
68 @functools.total_ordering
69 class Atom(object):
70     def __init__(self, type_, value=None):
71         self.type = type_
72         if value is not None:
73             self.value = value
74         else:
75             self.value = type_.default_atom()
76
77     def __eq__(self, other):
78         if not isinstance(other, Atom) or self.type != other.type:
79             return NotImplemented
80         return True if self.value == other.value else False
81
82     def __lt__(self, other):
83         if not isinstance(other, Atom) or self.type != other.type:
84             return NotImplemented
85         return True if self.value < other.value else False
86
87     def __cmp__(self, other):
88         if not isinstance(other, Atom) or self.type != other.type:
89             return NotImplemented
90         elif self.value < other.value:
91             return -1
92         elif self.value > other.value:
93             return 1
94         else:
95             return 0
96
97     def __hash__(self):
98         return hash(self.value)
99
100     @staticmethod
101     def default(type_):
102         """Returns the default value for the given type_, which must be an
103         instance of ovs.db.types.AtomicType.
104
105         The default value for each atomic type is;
106
107           - 0, for integer or real atoms.
108
109           - False, for a boolean atom.
110
111           - "", for a string atom.
112
113           - The all-zeros UUID, for a UUID atom."""
114         return Atom(type_)
115
116     def is_default(self):
117         return self == self.default(self.type)
118
119     @staticmethod
120     def from_json(base, json, symtab=None):
121         type_ = base.type
122         json = ovs.db.parser.float_to_int(json)
123         real_types = list(six.integer_types)
124         real_types.extend([float])
125         real_types = tuple(real_types)
126         if ((type_ == ovs.db.types.IntegerType
127                 and isinstance(json, six.integer_types))
128             or (type_ == ovs.db.types.RealType
129                 and isinstance(json, real_types))
130             or (type_ == ovs.db.types.BooleanType and isinstance(json, bool))
131             or (type_ == ovs.db.types.StringType
132                 and isinstance(json, six.string_types))):
133             atom = Atom(type_, json)
134         elif type_ == ovs.db.types.UuidType:
135             atom = Atom(type_, ovs.ovsuuid.from_json(json, symtab))
136         else:
137             raise error.Error("expected %s" % type_.to_string(), json)
138         atom.check_constraints(base)
139         return atom
140
141     @staticmethod
142     def from_python(base, value):
143         value = ovs.db.parser.float_to_int(value)
144         if isinstance(value, base.type.python_types):
145             atom = Atom(base.type, value)
146         else:
147             raise error.Error("expected %s, got %s" % (base.type, type(value)))
148         atom.check_constraints(base)
149         return atom
150
151     def check_constraints(self, base):
152         """Checks whether 'atom' meets the constraints (if any) defined in
153         'base' and raises an ovs.db.error.Error if any constraint is violated.
154
155         'base' and 'atom' must have the same type.
156         Checking UUID constraints is deferred to transaction commit time, so
157         this function does nothing for UUID constraints."""
158         assert base.type == self.type
159         if base.enum is not None and self not in base.enum:
160             raise ConstraintViolation(
161                 "%s is not one of the allowed values (%s)"
162                 % (self.to_string(), base.enum.to_string()))
163         elif base.type in [ovs.db.types.IntegerType, ovs.db.types.RealType]:
164             if ((base.min is None or self.value >= base.min) and
165                     (base.max is None or self.value <= base.max)):
166                 pass
167             elif base.min is not None and base.max is not None:
168                 raise ConstraintViolation(
169                     "%s is not in the valid range %.15g to %.15g (inclusive)"
170                     % (self.to_string(), base.min, base.max))
171             elif base.min is not None:
172                 raise ConstraintViolation(
173                     "%s is less than minimum allowed value %.15g"
174                     % (self.to_string(), base.min))
175             else:
176                 raise ConstraintViolation(
177                     "%s is greater than maximum allowed value %.15g"
178                     % (self.to_string(), base.max))
179         elif base.type == ovs.db.types.StringType:
180             # XXX The C version validates that the string is valid UTF-8 here.
181             # Do we need to do that in Python too?
182             s = self.value
183             length = len(s)
184             if length < base.min_length:
185                 raise ConstraintViolation(
186                     '"%s" length %d is less than minimum allowed length %d'
187                     % (s, length, base.min_length))
188             elif length > base.max_length:
189                 raise ConstraintViolation(
190                     '"%s" length %d is greater than maximum allowed '
191                     'length %d' % (s, length, base.max_length))
192
193     def to_json(self):
194         if self.type == ovs.db.types.UuidType:
195             return ovs.ovsuuid.to_json(self.value)
196         else:
197             return self.value
198
199     def cInitAtom(self, var):
200         if self.type == ovs.db.types.IntegerType:
201             return ['%s.integer = %d;' % (var, self.value)]
202         elif self.type == ovs.db.types.RealType:
203             return ['%s.real = %.15g;' % (var, self.value)]
204         elif self.type == ovs.db.types.BooleanType:
205             if self.value:
206                 return ['%s.boolean = true;']
207             else:
208                 return ['%s.boolean = false;']
209         elif self.type == ovs.db.types.StringType:
210             return ['%s.string = xstrdup("%s");'
211                     % (var, escapeCString(self.value))]
212         elif self.type == ovs.db.types.UuidType:
213             return ovs.ovsuuid.to_c_assignment(self.value, var)
214
215     def toEnglish(self, escapeLiteral=returnUnchanged):
216         if self.type == ovs.db.types.IntegerType:
217             return '%d' % self.value
218         elif self.type == ovs.db.types.RealType:
219             return '%.15g' % self.value
220         elif self.type == ovs.db.types.BooleanType:
221             if self.value:
222                 return 'true'
223             else:
224                 return 'false'
225         elif self.type == ovs.db.types.StringType:
226             return escapeLiteral(self.value)
227         elif self.type == ovs.db.types.UuidType:
228             return self.value.value
229
230     __need_quotes_re = re.compile("$|true|false|[^_a-zA-Z]|.*[^-._a-zA-Z]")
231
232     @staticmethod
233     def __string_needs_quotes(s):
234         return Atom.__need_quotes_re.match(s)
235
236     def to_string(self):
237         if self.type == ovs.db.types.IntegerType:
238             return '%d' % self.value
239         elif self.type == ovs.db.types.RealType:
240             return '%.15g' % self.value
241         elif self.type == ovs.db.types.BooleanType:
242             if self.value:
243                 return 'true'
244             else:
245                 return 'false'
246         elif self.type == ovs.db.types.StringType:
247             if Atom.__string_needs_quotes(self.value):
248                 return ovs.json.to_string(self.value)
249             else:
250                 return self.value
251         elif self.type == ovs.db.types.UuidType:
252             return str(self.value)
253
254     @staticmethod
255     def new(x):
256         if isinstance(x, six.integer_types):
257             t = ovs.db.types.IntegerType
258         elif isinstance(x, float):
259             t = ovs.db.types.RealType
260         elif isinstance(x, bool):
261             t = ovs.db.types.BooleanType
262         elif isinstance(x, six.string_types):
263             t = ovs.db.types.StringType
264         elif isinstance(x, uuid):
265             t = ovs.db.types.UuidType
266         else:
267             raise TypeError
268         return Atom(t, x)
269
270
271 @functools.total_ordering
272 class Datum(object):
273     def __init__(self, type_, values={}):
274         self.type = type_
275         self.values = values
276
277     def __eq__(self, other):
278         if not isinstance(other, Datum):
279             return NotImplemented
280         return True if self.values == other.values else False
281
282     def __lt__(self, other):
283         if not isinstance(other, Datum):
284             return NotImplemented
285         return True if self.values < other.values else False
286
287     def __cmp__(self, other):
288         if not isinstance(other, Datum):
289             return NotImplemented
290         elif self.values < other.values:
291             return -1
292         elif self.values > other.values:
293             return 1
294         else:
295             return 0
296
297     __hash__ = None
298
299     def __contains__(self, item):
300         return item in self.values
301
302     def copy(self):
303         return Datum(self.type, dict(self.values))
304
305     @staticmethod
306     def default(type_):
307         if type_.n_min == 0:
308             values = {}
309         elif type_.is_map():
310             values = {type_.key.default(): type_.value.default()}
311         else:
312             values = {type_.key.default(): None}
313         return Datum(type_, values)
314
315     def is_default(self):
316         return self == Datum.default(self.type)
317
318     def check_constraints(self):
319         """Checks that each of the atoms in 'datum' conforms to the constraints
320         specified by its 'type' and raises an ovs.db.error.Error.
321
322         This function is not commonly useful because the most ordinary way to
323         obtain a datum is ultimately via Datum.from_json() or Atom.from_json(),
324         which check constraints themselves."""
325         for keyAtom, valueAtom in six.iteritems(self.values):
326             keyAtom.check_constraints(self.type.key)
327             if valueAtom is not None:
328                 valueAtom.check_constraints(self.type.value)
329
330     @staticmethod
331     def from_json(type_, json, symtab=None):
332         """Parses 'json' as a datum of the type described by 'type'.  If
333         successful, returns a new datum.  On failure, raises an
334         ovs.db.error.Error.
335
336         Violations of constraints expressed by 'type' are treated as errors.
337
338         If 'symtab' is nonnull, then named UUIDs in 'symtab' are accepted.
339         Refer to RFC 7047 for information about this, and for the syntax
340         that this function accepts."""
341         is_map = type_.is_map()
342         if (is_map or
343             (isinstance(json, list) and len(json) > 0 and json[0] == "set")):
344             if is_map:
345                 class_ = "map"
346             else:
347                 class_ = "set"
348
349             inner = ovs.db.parser.unwrap_json(json, class_, [list, tuple],
350                                               "array")
351             n = len(inner)
352             if n < type_.n_min or n > type_.n_max:
353                 raise error.Error("%s must have %d to %d members but %d are "
354                                   "present" % (class_, type_.n_min,
355                                                type_.n_max, n),
356                                   json)
357
358             values = {}
359             for element in inner:
360                 if is_map:
361                     key, value = ovs.db.parser.parse_json_pair(element)
362                     keyAtom = Atom.from_json(type_.key, key, symtab)
363                     valueAtom = Atom.from_json(type_.value, value, symtab)
364                 else:
365                     keyAtom = Atom.from_json(type_.key, element, symtab)
366                     valueAtom = None
367
368                 if keyAtom in values:
369                     if is_map:
370                         raise error.Error("map contains duplicate key")
371                     else:
372                         raise error.Error("set contains duplicate")
373
374                 values[keyAtom] = valueAtom
375
376             return Datum(type_, values)
377         else:
378             keyAtom = Atom.from_json(type_.key, json, symtab)
379             return Datum(type_, {keyAtom: None})
380
381     def to_json(self):
382         if self.type.is_map():
383             return ["map", [[k.to_json(), v.to_json()]
384                             for k, v in sorted(self.values.items())]]
385         elif len(self.values) == 1:
386             key = next(six.iterkeys(self.values))
387             return key.to_json()
388         else:
389             return ["set", [k.to_json() for k in sorted(self.values.keys())]]
390
391     def to_string(self):
392         head = tail = None
393         if self.type.n_max > 1 or len(self.values) == 0:
394             if self.type.is_map():
395                 head = "{"
396                 tail = "}"
397             else:
398                 head = "["
399                 tail = "]"
400
401         s = []
402         if head:
403             s.append(head)
404
405         for i, key in enumerate(sorted(self.values)):
406             if i:
407                 s.append(", ")
408
409             s.append(key.to_string())
410             if self.type.is_map():
411                 s.append("=")
412                 s.append(self.values[key].to_string())
413
414         if tail:
415             s.append(tail)
416         return ''.join(s)
417
418     def diff(self, datum):
419         if self.type.n_max > 1 or len(self.values) == 0:
420             for k, v in six.iteritems(datum.values):
421                 if k in self.values and v == self.values[k]:
422                     del self.values[k]
423                 else:
424                     self.values[k] = v
425         else:
426             return datum
427
428         return self
429
430     def as_list(self):
431         if self.type.is_map():
432             return [[k.value, v.value] for k, v in six.iteritems(self.values)]
433         else:
434             return [k.value for k in six.iterkeys(self.values)]
435
436     def as_dict(self):
437         return dict(self.values)
438
439     def as_scalar(self):
440         if len(self.values) == 1:
441             if self.type.is_map():
442                 k, v = next(six.iteritems(self.values))
443                 return [k.value, v.value]
444             else:
445                 return next(six.iterkeys(self.values)).value
446         else:
447             return None
448
449     def to_python(self, uuid_to_row):
450         """Returns this datum's value converted into a natural Python
451         representation of this datum's type, according to the following
452         rules:
453
454         - If the type has exactly one value and it is not a map (that is,
455           self.type.is_scalar() returns True), then the value is:
456
457             * An int or long, for an integer column.
458
459             * An int or long or float, for a real column.
460
461             * A bool, for a boolean column.
462
463             * A str or unicode object, for a string column.
464
465             * A uuid.UUID object, for a UUID column without a ref_table.
466
467             * An object represented the referenced row, for a UUID column with
468               a ref_table.  (For the Idl, this object will be an ovs.db.idl.Row
469               object.)
470
471           If some error occurs (e.g. the database server's idea of the column
472           is different from the IDL's idea), then the default value for the
473           scalar type is used (see Atom.default()).
474
475         - Otherwise, if the type is not a map, then the value is a Python list
476           whose elements have the types described above.
477
478         - Otherwise, the type is a map, and the value is a Python dict that
479           maps from key to value, with key and value types determined as
480           described above.
481
482         'uuid_to_row' must be a function that takes a value and an
483         ovs.db.types.BaseType and translates UUIDs into row objects."""
484         if self.type.is_scalar():
485             value = uuid_to_row(self.as_scalar(), self.type.key)
486             if value is None:
487                 return self.type.key.default()
488             else:
489                 return value
490         elif self.type.is_map():
491             value = {}
492             for k, v in six.iteritems(self.values):
493                 dk = uuid_to_row(k.value, self.type.key)
494                 dv = uuid_to_row(v.value, self.type.value)
495                 if dk is not None and dv is not None:
496                     value[dk] = dv
497             return value
498         else:
499             s = set()
500             for k in self.values:
501                 dk = uuid_to_row(k.value, self.type.key)
502                 if dk is not None:
503                     s.add(dk)
504             return sorted(s)
505
506     @staticmethod
507     def from_python(type_, value, row_to_uuid):
508         """Returns a new Datum with the given ovs.db.types.Type 'type_'.  The
509         new datum's value is taken from 'value', which must take the form
510         described as a valid return value from Datum.to_python() for 'type'.
511
512         Each scalar value within 'value' is initially passed through
513         'row_to_uuid', which should convert objects that represent rows (if
514         any) into uuid.UUID objects and return other data unchanged.
515
516         Raises ovs.db.error.Error if 'value' is not in an appropriate form for
517         'type_'."""
518         d = {}
519         if isinstance(value, dict):
520             for k, v in six.iteritems(value):
521                 ka = Atom.from_python(type_.key, row_to_uuid(k))
522                 va = Atom.from_python(type_.value, row_to_uuid(v))
523                 d[ka] = va
524         elif isinstance(value, (list, set, tuple)):
525             for k in value:
526                 ka = Atom.from_python(type_.key, row_to_uuid(k))
527                 d[ka] = None
528         else:
529             ka = Atom.from_python(type_.key, row_to_uuid(value))
530             d[ka] = None
531
532         datum = Datum(type_, d)
533         datum.check_constraints()
534         if not datum.conforms_to_type():
535             raise error.Error("%d values when type requires between %d and %d"
536                               % (len(d), type_.n_min, type_.n_max))
537
538         return datum
539
540     def __getitem__(self, key):
541         if not isinstance(key, Atom):
542             key = Atom.new(key)
543         if not self.type.is_map():
544             raise IndexError
545         elif key not in self.values:
546             raise KeyError
547         else:
548             return self.values[key].value
549
550     def get(self, key, default=None):
551         if not isinstance(key, Atom):
552             key = Atom.new(key)
553         if key in self.values:
554             return self.values[key].value
555         else:
556             return default
557
558     def __str__(self):
559         return self.to_string()
560
561     def conforms_to_type(self):
562         n = len(self.values)
563         return self.type.n_min <= n <= self.type.n_max
564
565     def cInitDatum(self, var):
566         if len(self.values) == 0:
567             return ["ovsdb_datum_init_empty(%s);" % var]
568
569         s = ["%s->n = %d;" % (var, len(self.values))]
570         s += ["%s->keys = xmalloc(%d * sizeof *%s->keys);"
571               % (var, len(self.values), var)]
572
573         for i, key in enumerate(sorted(self.values)):
574             s += key.cInitAtom("%s->keys[%d]" % (var, i))
575
576         if self.type.value:
577             s += ["%s->values = xmalloc(%d * sizeof *%s->values);"
578                   % (var, len(self.values), var)]
579             for i, (key, value) in enumerate(sorted(self.values.items())):
580                 s += value.cInitAtom("%s->values[%d]" % (var, i))
581         else:
582             s += ["%s->values = NULL;" % var]
583
584         if len(self.values) > 1:
585             s += ["ovsdb_datum_sort_assert(%s, OVSDB_TYPE_%s);"
586                   % (var, self.type.key.type.to_string().upper())]
587
588         return s