Synchronise the openstack bugs
[parser.git] / tosca2heat / tosca-parser / toscaparser / elements / constraints.py
1 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
2 #    not use this file except in compliance with the License. You may obtain
3 #    a copy of the License at
4 #
5 #         http://www.apache.org/licenses/LICENSE-2.0
6 #
7 #    Unless required by applicable law or agreed to in writing, software
8 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 #    License for the specific language governing permissions and limitations
11 #    under the License.
12
13 import collections
14 import datetime
15 import re
16
17 import toscaparser
18 from toscaparser.common.exception import ExceptionCollector
19 from toscaparser.common.exception import InvalidSchemaError
20 from toscaparser.common.exception import ValidationError
21 from toscaparser.elements.portspectype import PortSpec
22 from toscaparser.elements import scalarunit
23 from toscaparser.utils.gettextutils import _
24
25
26 class Schema(collections.Mapping):
27
28     KEYS = (
29         TYPE, REQUIRED, DESCRIPTION,
30         DEFAULT, CONSTRAINTS, ENTRYSCHEMA
31     ) = (
32         'type', 'required', 'description',
33         'default', 'constraints', 'entry_schema'
34     )
35
36     PROPERTY_TYPES = (
37         INTEGER, STRING, BOOLEAN, FLOAT, RANGE,
38         NUMBER, TIMESTAMP, LIST, MAP,
39         SCALAR_UNIT_SIZE, SCALAR_UNIT_FREQUENCY, SCALAR_UNIT_TIME,
40         VERSION, PORTDEF, PORTSPEC
41     ) = (
42         'integer', 'string', 'boolean', 'float', 'range',
43         'number', 'timestamp', 'list', 'map',
44         'scalar-unit.size', 'scalar-unit.frequency', 'scalar-unit.time',
45         'version', 'PortDef', PortSpec.SHORTNAME
46     )
47
48     SCALAR_UNIT_SIZE_DEFAULT = 'B'
49     SCALAR_UNIT_SIZE_DICT = {'B': 1, 'KB': 1000, 'KIB': 1024, 'MB': 1000000,
50                              'MIB': 1048576, 'GB': 1000000000,
51                              'GIB': 1073741824, 'TB': 1000000000000,
52                              'TIB': 1099511627776}
53
54     def __init__(self, name, schema_dict):
55         self.name = name
56         if not isinstance(schema_dict, collections.Mapping):
57             msg = (_('Schema definition of "%(pname)s" must be a dict.')
58                    % dict(pname=name))
59             ExceptionCollector.appendException(InvalidSchemaError(message=msg))
60
61         try:
62             schema_dict['type']
63         except KeyError:
64             msg = (_('Schema definition of "%(pname)s" must have a "type" '
65                      'attribute.') % dict(pname=name))
66             ExceptionCollector.appendException(InvalidSchemaError(message=msg))
67
68         self.schema = schema_dict
69         self._len = None
70         self.constraints_list = []
71
72     @property
73     def type(self):
74         return self.schema[self.TYPE]
75
76     @property
77     def required(self):
78         return self.schema.get(self.REQUIRED, True)
79
80     @property
81     def description(self):
82         return self.schema.get(self.DESCRIPTION, '')
83
84     @property
85     def default(self):
86         return self.schema.get(self.DEFAULT)
87
88     @property
89     def constraints(self):
90         if not self.constraints_list:
91             constraint_schemata = self.schema.get(self.CONSTRAINTS)
92             if constraint_schemata:
93                 self.constraints_list = [Constraint(self.name,
94                                                     self.type,
95                                                     cschema)
96                                          for cschema in constraint_schemata]
97         return self.constraints_list
98
99     @property
100     def entry_schema(self):
101         return self.schema.get(self.ENTRYSCHEMA)
102
103     def __getitem__(self, key):
104         return self.schema[key]
105
106     def __iter__(self):
107         for k in self.KEYS:
108             try:
109                 self.schema[k]
110             except KeyError:
111                 pass
112             else:
113                 yield k
114
115     def __len__(self):
116         if self._len is None:
117             self._len = len(list(iter(self)))
118         return self._len
119
120
121 class Constraint(object):
122     '''Parent class for constraints for a Property or Input.'''
123
124     CONSTRAINTS = (EQUAL, GREATER_THAN,
125                    GREATER_OR_EQUAL, LESS_THAN, LESS_OR_EQUAL, IN_RANGE,
126                    VALID_VALUES, LENGTH, MIN_LENGTH, MAX_LENGTH, PATTERN) = \
127                   ('equal', 'greater_than', 'greater_or_equal', 'less_than',
128                    'less_or_equal', 'in_range', 'valid_values', 'length',
129                    'min_length', 'max_length', 'pattern')
130
131     def __new__(cls, property_name, property_type, constraint):
132         if cls is not Constraint:
133             return super(Constraint, cls).__new__(cls)
134
135         if(not isinstance(constraint, collections.Mapping) or
136            len(constraint) != 1):
137             ExceptionCollector.appendException(
138                 InvalidSchemaError(message=_('Invalid constraint schema.')))
139
140         for type in constraint.keys():
141             ConstraintClass = get_constraint_class(type)
142             if not ConstraintClass:
143                 msg = _('Invalid property "%s".') % type
144                 ExceptionCollector.appendException(
145                     InvalidSchemaError(message=msg))
146
147         return ConstraintClass(property_name, property_type, constraint)
148
149     def __init__(self, property_name, property_type, constraint):
150         self.property_name = property_name
151         self.property_type = property_type
152         self.constraint_value = constraint[self.constraint_key]
153         self.constraint_value_msg = self.constraint_value
154         if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES:
155             self.constraint_value = self._get_scalarunit_constraint_value()
156         # check if constraint is valid for property type
157         if property_type not in self.valid_prop_types:
158             msg = _('Property "%(ctype)s" is not valid for data type '
159                     '"%(dtype)s".') % dict(
160                         ctype=self.constraint_key,
161                         dtype=property_type)
162             ExceptionCollector.appendException(InvalidSchemaError(message=msg))
163
164     def _get_scalarunit_constraint_value(self):
165         if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES:
166             ScalarUnit_Class = (scalarunit.
167                                 get_scalarunit_class(self.property_type))
168         if isinstance(self.constraint_value, list):
169             return [ScalarUnit_Class(v).get_num_from_scalar_unit()
170                     for v in self.constraint_value]
171         else:
172             return (ScalarUnit_Class(self.constraint_value).
173                     get_num_from_scalar_unit())
174
175     def _err_msg(self, value):
176         return _('Property "%s" could not be validated.') % self.property_name
177
178     def validate(self, value):
179         self.value_msg = value
180         if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES:
181             value = scalarunit.get_scalarunit_value(self.property_type, value)
182         if not self._is_valid(value):
183             err_msg = self._err_msg(value)
184             ExceptionCollector.appendException(
185                 ValidationError(message=err_msg))
186
187
188 class Equal(Constraint):
189     """Constraint class for "equal"
190
191     Constrains a property or parameter to a value equal to ('=')
192     the value declared.
193     """
194
195     constraint_key = Constraint.EQUAL
196
197     valid_prop_types = Schema.PROPERTY_TYPES
198
199     def _is_valid(self, value):
200         if value == self.constraint_value:
201             return True
202
203         return False
204
205     def _err_msg(self, value):
206         return (_('The value "%(pvalue)s" of property "%(pname)s" is not '
207                   'equal to "%(cvalue)s".') %
208                 dict(pname=self.property_name,
209                      pvalue=self.value_msg,
210                      cvalue=self.constraint_value_msg))
211
212
213 class GreaterThan(Constraint):
214     """Constraint class for "greater_than"
215
216     Constrains a property or parameter to a value greater than ('>')
217     the value declared.
218     """
219
220     constraint_key = Constraint.GREATER_THAN
221
222     valid_types = (int, float, datetime.date,
223                    datetime.time, datetime.datetime)
224
225     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
226                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
227                         Schema.SCALAR_UNIT_TIME)
228
229     def __init__(self, property_name, property_type, constraint):
230         super(GreaterThan, self).__init__(property_name, property_type,
231                                           constraint)
232         if not isinstance(constraint[self.GREATER_THAN], self.valid_types):
233             ExceptionCollector.appendException(
234                 InvalidSchemaError(message=_('The property "greater_than" '
235                                              'expects comparable values.')))
236
237     def _is_valid(self, value):
238         if value > self.constraint_value:
239             return True
240
241         return False
242
243     def _err_msg(self, value):
244         return (_('The value "%(pvalue)s" of property "%(pname)s" must be '
245                   'greater than "%(cvalue)s".') %
246                 dict(pname=self.property_name,
247                      pvalue=self.value_msg,
248                      cvalue=self.constraint_value_msg))
249
250
251 class GreaterOrEqual(Constraint):
252     """Constraint class for "greater_or_equal"
253
254     Constrains a property or parameter to a value greater than or equal
255     to ('>=') the value declared.
256     """
257
258     constraint_key = Constraint.GREATER_OR_EQUAL
259
260     valid_types = (int, float, datetime.date,
261                    datetime.time, datetime.datetime)
262
263     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
264                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
265                         Schema.SCALAR_UNIT_TIME)
266
267     def __init__(self, property_name, property_type, constraint):
268         super(GreaterOrEqual, self).__init__(property_name, property_type,
269                                              constraint)
270         if not isinstance(self.constraint_value, self.valid_types):
271             ExceptionCollector.appendException(
272                 InvalidSchemaError(message=_('The property '
273                                              '"greater_or_equal" expects '
274                                              'comparable values.')))
275
276     def _is_valid(self, value):
277         if toscaparser.functions.is_function(value) or \
278            value >= self.constraint_value:
279             return True
280         return False
281
282     def _err_msg(self, value):
283         return (_('The value "%(pvalue)s" of property "%(pname)s" must be '
284                   'greater than or equal to "%(cvalue)s".') %
285                 dict(pname=self.property_name,
286                      pvalue=self.value_msg,
287                      cvalue=self.constraint_value_msg))
288
289
290 class LessThan(Constraint):
291     """Constraint class for "less_than"
292
293     Constrains a property or parameter to a value less than ('<')
294     the value declared.
295     """
296
297     constraint_key = Constraint.LESS_THAN
298
299     valid_types = (int, float, datetime.date,
300                    datetime.time, datetime.datetime)
301
302     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
303                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
304                         Schema.SCALAR_UNIT_TIME)
305
306     def __init__(self, property_name, property_type, constraint):
307         super(LessThan, self).__init__(property_name, property_type,
308                                        constraint)
309         if not isinstance(self.constraint_value, self.valid_types):
310             ExceptionCollector.appendException(
311                 InvalidSchemaError(message=_('The property "less_than" '
312                                              'expects comparable values.')))
313
314     def _is_valid(self, value):
315         if value < self.constraint_value:
316             return True
317
318         return False
319
320     def _err_msg(self, value):
321         return (_('The value "%(pvalue)s" of property "%(pname)s" must be '
322                   'less than "%(cvalue)s".') %
323                 dict(pname=self.property_name,
324                      pvalue=self.value_msg,
325                      cvalue=self.constraint_value_msg))
326
327
328 class LessOrEqual(Constraint):
329     """Constraint class for "less_or_equal"
330
331     Constrains a property or parameter to a value less than or equal
332     to ('<=') the value declared.
333     """
334
335     constraint_key = Constraint.LESS_OR_EQUAL
336
337     valid_types = (int, float, datetime.date,
338                    datetime.time, datetime.datetime)
339
340     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
341                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
342                         Schema.SCALAR_UNIT_TIME)
343
344     def __init__(self, property_name, property_type, constraint):
345         super(LessOrEqual, self).__init__(property_name, property_type,
346                                           constraint)
347         if not isinstance(self.constraint_value, self.valid_types):
348             ExceptionCollector.appendException(
349                 InvalidSchemaError(message=_('The property "less_or_equal" '
350                                              'expects comparable values.')))
351
352     def _is_valid(self, value):
353         if value <= self.constraint_value:
354             return True
355
356         return False
357
358     def _err_msg(self, value):
359         return (_('The value "%(pvalue)s" of property "%(pname)s" must be '
360                   'less than or equal to "%(cvalue)s".') %
361                 dict(pname=self.property_name,
362                      pvalue=self.value_msg,
363                      cvalue=self.constraint_value_msg))
364
365
366 class InRange(Constraint):
367     """Constraint class for "in_range"
368
369     Constrains a property or parameter to a value in range of (inclusive)
370     the two values declared.
371     """
372     UNBOUNDED = 'UNBOUNDED'
373
374     constraint_key = Constraint.IN_RANGE
375
376     valid_types = (int, float, datetime.date,
377                    datetime.time, datetime.datetime, str)
378
379     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
380                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
381                         Schema.SCALAR_UNIT_TIME, Schema.RANGE)
382
383     def __init__(self, property_name, property_type, constraint):
384         super(InRange, self).__init__(property_name, property_type, constraint)
385         if(not isinstance(self.constraint_value, collections.Sequence) or
386            (len(constraint[self.IN_RANGE]) != 2)):
387             ExceptionCollector.appendException(
388                 InvalidSchemaError(message=_('The property "in_range" '
389                                              'expects a list.')))
390
391         msg = _('The property "in_range" expects comparable values.')
392         for value in self.constraint_value:
393             if not isinstance(value, self.valid_types):
394                 ExceptionCollector.appendException(
395                     InvalidSchemaError(message=msg))
396             # The only string we allow for range is the special value
397             # 'UNBOUNDED'
398             if(isinstance(value, str) and value != self.UNBOUNDED):
399                 ExceptionCollector.appendException(
400                     InvalidSchemaError(message=msg))
401
402         self.min = self.constraint_value[0]
403         self.max = self.constraint_value[1]
404
405     def _is_valid(self, value):
406         if not isinstance(self.min, str):
407             if value < self.min:
408                 return False
409         elif self.min != self.UNBOUNDED:
410             return False
411         if not isinstance(self.max, str):
412             if value > self.max:
413                 return False
414         elif self.max != self.UNBOUNDED:
415             return False
416         return True
417
418     def _err_msg(self, value):
419         return (_('The value "%(pvalue)s" of property "%(pname)s" is out of '
420                   'range "(min:%(vmin)s, max:%(vmax)s)".') %
421                 dict(pname=self.property_name,
422                      pvalue=self.value_msg,
423                      vmin=self.constraint_value_msg[0],
424                      vmax=self.constraint_value_msg[1]))
425
426
427 class ValidValues(Constraint):
428     """Constraint class for "valid_values"
429
430     Constrains a property or parameter to a value that is in the list of
431     declared values.
432     """
433     constraint_key = Constraint.VALID_VALUES
434
435     valid_prop_types = Schema.PROPERTY_TYPES
436
437     def __init__(self, property_name, property_type, constraint):
438         super(ValidValues, self).__init__(property_name, property_type,
439                                           constraint)
440         if not isinstance(self.constraint_value, collections.Sequence):
441             ExceptionCollector.appendException(
442                 InvalidSchemaError(message=_('The property "valid_values" '
443                                              'expects a list.')))
444
445     def _is_valid(self, value):
446         if isinstance(value, list):
447             return all(v in self.constraint_value for v in value)
448         return value in self.constraint_value
449
450     def _err_msg(self, value):
451         allowed = '[%s]' % ', '.join(str(a) for a in self.constraint_value)
452         return (_('The value "%(pvalue)s" of property "%(pname)s" is not '
453                   'valid. Expected a value from "%(cvalue)s".') %
454                 dict(pname=self.property_name,
455                      pvalue=value,
456                      cvalue=allowed))
457
458
459 class Length(Constraint):
460     """Constraint class for "length"
461
462     Constrains the property or parameter to a value of a given length.
463     """
464
465     constraint_key = Constraint.LENGTH
466
467     valid_types = (int, )
468
469     valid_prop_types = (Schema.STRING, )
470
471     def __init__(self, property_name, property_type, constraint):
472         super(Length, self).__init__(property_name, property_type, constraint)
473         if not isinstance(self.constraint_value, self.valid_types):
474             ExceptionCollector.appendException(
475                 InvalidSchemaError(message=_('The property "length" expects '
476                                              'an integer.')))
477
478     def _is_valid(self, value):
479         if isinstance(value, str) and len(value) == self.constraint_value:
480             return True
481
482         return False
483
484     def _err_msg(self, value):
485         return (_('Length of value "%(pvalue)s" of property "%(pname)s" '
486                   'must be equal to "%(cvalue)s".') %
487                 dict(pname=self.property_name,
488                      pvalue=value,
489                      cvalue=self.constraint_value))
490
491
492 class MinLength(Constraint):
493     """Constraint class for "min_length"
494
495     Constrains the property or parameter to a value to a minimum length.
496     """
497
498     constraint_key = Constraint.MIN_LENGTH
499
500     valid_types = (int, )
501
502     valid_prop_types = (Schema.STRING, Schema.MAP)
503
504     def __init__(self, property_name, property_type, constraint):
505         super(MinLength, self).__init__(property_name, property_type,
506                                         constraint)
507         if not isinstance(self.constraint_value, self.valid_types):
508             ExceptionCollector.appendException(
509                 InvalidSchemaError(message=_('The property "min_length" '
510                                              'expects an integer.')))
511
512     def _is_valid(self, value):
513         if ((isinstance(value, str) or isinstance(value, dict)) and
514            len(value) >= self.constraint_value):
515             return True
516
517         return False
518
519     def _err_msg(self, value):
520         return (_('Length of value "%(pvalue)s" of property "%(pname)s" '
521                   'must be at least "%(cvalue)s".') %
522                 dict(pname=self.property_name,
523                      pvalue=value,
524                      cvalue=self.constraint_value))
525
526
527 class MaxLength(Constraint):
528     """Constraint class for "max_length"
529
530     Constrains the property or parameter to a value to a maximum length.
531     """
532
533     constraint_key = Constraint.MAX_LENGTH
534
535     valid_types = (int, )
536
537     valid_prop_types = (Schema.STRING, Schema.MAP)
538
539     def __init__(self, property_name, property_type, constraint):
540         super(MaxLength, self).__init__(property_name, property_type,
541                                         constraint)
542         if not isinstance(self.constraint_value, self.valid_types):
543             ExceptionCollector.appendException(
544                 InvalidSchemaError(message=_('The property "max_length" '
545                                              'expects an integer.')))
546
547     def _is_valid(self, value):
548         if ((isinstance(value, str) or isinstance(value, dict)) and
549            len(value) <= self.constraint_value):
550             return True
551
552         return False
553
554     def _err_msg(self, value):
555         return (_('Length of value "%(pvalue)s" of property "%(pname)s" '
556                   'must be no greater than "%(cvalue)s".') %
557                 dict(pname=self.property_name,
558                      pvalue=value,
559                      cvalue=self.constraint_value))
560
561
562 class Pattern(Constraint):
563     """Constraint class for "pattern"
564
565     Constrains the property or parameter to a value that is allowed by
566     the provided regular expression.
567     """
568
569     constraint_key = Constraint.PATTERN
570
571     valid_types = (str, )
572
573     valid_prop_types = (Schema.STRING, )
574
575     def __init__(self, property_name, property_type, constraint):
576         super(Pattern, self).__init__(property_name, property_type, constraint)
577         if not isinstance(self.constraint_value, self.valid_types):
578             ExceptionCollector.appendException(
579                 InvalidSchemaError(message=_('The property "pattern" '
580                                              'expects a string.')))
581         self.match = re.compile(self.constraint_value).match
582
583     def _is_valid(self, value):
584         match = self.match(value)
585         return match is not None and match.end() == len(value)
586
587     def _err_msg(self, value):
588         return (_('The value "%(pvalue)s" of property "%(pname)s" does not '
589                   'match pattern "%(cvalue)s".') %
590                 dict(pname=self.property_name,
591                      pvalue=value,
592                      cvalue=self.constraint_value))
593
594
595 constraint_mapping = {
596     Constraint.EQUAL: Equal,
597     Constraint.GREATER_THAN: GreaterThan,
598     Constraint.GREATER_OR_EQUAL: GreaterOrEqual,
599     Constraint.LESS_THAN: LessThan,
600     Constraint.LESS_OR_EQUAL: LessOrEqual,
601     Constraint.IN_RANGE: InRange,
602     Constraint.VALID_VALUES: ValidValues,
603     Constraint.LENGTH: Length,
604     Constraint.MIN_LENGTH: MinLength,
605     Constraint.MAX_LENGTH: MaxLength,
606     Constraint.PATTERN: Pattern
607     }
608
609
610 def get_constraint_class(type):
611     return constraint_mapping.get(type)