docs: fix issues
[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 import scalarunit
22 from toscaparser.utils.gettextutils import _
23
24
25 class Schema(collections.Mapping):
26
27     KEYS = (
28         TYPE, REQUIRED, DESCRIPTION,
29         DEFAULT, CONSTRAINTS, ENTRYSCHEMA
30     ) = (
31         'type', 'required', 'description',
32         'default', 'constraints', 'entry_schema'
33     )
34
35     PROPERTY_TYPES = (
36         INTEGER, STRING, BOOLEAN, FLOAT, RANGE,
37         NUMBER, TIMESTAMP, LIST, MAP,
38         SCALAR_UNIT_SIZE, SCALAR_UNIT_FREQUENCY, SCALAR_UNIT_TIME,
39         PORTDEF, VERSION
40     ) = (
41         'integer', 'string', 'boolean', 'float', 'range',
42         'number', 'timestamp', 'list', 'map',
43         'scalar-unit.size', 'scalar-unit.frequency', 'scalar-unit.time',
44         'PortDef', 'version'
45     )
46
47     SCALAR_UNIT_SIZE_DEFAULT = 'B'
48     SCALAR_UNIT_SIZE_DICT = {'B': 1, 'KB': 1000, 'KIB': 1024, 'MB': 1000000,
49                              'MIB': 1048576, 'GB': 1000000000,
50                              'GIB': 1073741824, 'TB': 1000000000000,
51                              'TIB': 1099511627776}
52
53     def __init__(self, name, schema_dict):
54         self.name = name
55         if not isinstance(schema_dict, collections.Mapping):
56             msg = (_('Schema definition of "%(pname)s" must be a dict.')
57                    % dict(pname=name))
58             ExceptionCollector.appendException(InvalidSchemaError(message=msg))
59
60         try:
61             schema_dict['type']
62         except KeyError:
63             msg = (_('Schema definition of "%(pname)s" must have a "type" '
64                      'attribute.') % dict(pname=name))
65             ExceptionCollector.appendException(InvalidSchemaError(message=msg))
66
67         self.schema = schema_dict
68         self._len = None
69         self.constraints_list = []
70
71     @property
72     def type(self):
73         return self.schema[self.TYPE]
74
75     @property
76     def required(self):
77         return self.schema.get(self.REQUIRED, True)
78
79     @property
80     def description(self):
81         return self.schema.get(self.DESCRIPTION, '')
82
83     @property
84     def default(self):
85         return self.schema.get(self.DEFAULT)
86
87     @property
88     def constraints(self):
89         if not self.constraints_list:
90             constraint_schemata = self.schema.get(self.CONSTRAINTS)
91             if constraint_schemata:
92                 self.constraints_list = [Constraint(self.name,
93                                                     self.type,
94                                                     cschema)
95                                          for cschema in constraint_schemata]
96         return self.constraints_list
97
98     @property
99     def entry_schema(self):
100         return self.schema.get(self.ENTRYSCHEMA)
101
102     def __getitem__(self, key):
103         return self.schema[key]
104
105     def __iter__(self):
106         for k in self.KEYS:
107             try:
108                 self.schema[k]
109             except KeyError:
110                 pass
111             else:
112                 yield k
113
114     def __len__(self):
115         if self._len is None:
116             self._len = len(list(iter(self)))
117         return self._len
118
119
120 class Constraint(object):
121     '''Parent class for constraints for a Property or Input.'''
122
123     CONSTRAINTS = (EQUAL, GREATER_THAN,
124                    GREATER_OR_EQUAL, LESS_THAN, LESS_OR_EQUAL, IN_RANGE,
125                    VALID_VALUES, LENGTH, MIN_LENGTH, MAX_LENGTH, PATTERN) = \
126                   ('equal', 'greater_than', 'greater_or_equal', 'less_than',
127                    'less_or_equal', 'in_range', 'valid_values', 'length',
128                    'min_length', 'max_length', 'pattern')
129
130     UNBOUNDED = 'UNBOUNDED'
131
132     def __new__(cls, property_name, property_type, constraint):
133         if cls is not Constraint:
134             return super(Constraint, cls).__new__(cls)
135
136         if(not isinstance(constraint, collections.Mapping) or
137            len(constraint) != 1):
138             ExceptionCollector.appendException(
139                 InvalidSchemaError(message=_('Invalid constraint schema.')))
140
141         for type in constraint.keys():
142             ConstraintClass = get_constraint_class(type)
143             if not ConstraintClass:
144                 msg = _('Invalid property "%s".') % type
145                 ExceptionCollector.appendException(
146                     InvalidSchemaError(message=msg))
147
148         return ConstraintClass(property_name, property_type, constraint)
149
150     def __init__(self, property_name, property_type, constraint):
151         self.property_name = property_name
152         self.property_type = property_type
153         self.constraint_value = constraint[self.constraint_key]
154         self.constraint_value_msg = self.constraint_value
155         if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES:
156             self.constraint_value = self._get_scalarunit_constraint_value()
157         # check if constraint is valid for property type
158         if property_type not in self.valid_prop_types:
159             msg = _('Property "%(ctype)s" is not valid for data type '
160                     '"%(dtype)s".') % dict(
161                         ctype=self.constraint_key,
162                         dtype=property_type)
163             ExceptionCollector.appendException(InvalidSchemaError(message=msg))
164
165     def _get_scalarunit_constraint_value(self):
166         if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES:
167             ScalarUnit_Class = (scalarunit.
168                                 get_scalarunit_class(self.property_type))
169         if isinstance(self.constraint_value, list):
170             return [ScalarUnit_Class(v).get_num_from_scalar_unit()
171                     for v in self.constraint_value]
172         else:
173             return (ScalarUnit_Class(self.constraint_value).
174                     get_num_from_scalar_unit())
175
176     def _err_msg(self, value):
177         return _('Property "%s" could not be validated.') % self.property_name
178
179     def validate(self, value):
180         self.value_msg = value
181         if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES:
182             value = scalarunit.get_scalarunit_value(self.property_type, value)
183         if not self._is_valid(value):
184             err_msg = self._err_msg(value)
185             ExceptionCollector.appendException(
186                 ValidationError(message=err_msg))
187
188
189 class Equal(Constraint):
190     """Constraint class for "equal"
191
192     Constrains a property or parameter to a value equal to ('=')
193     the value declared.
194     """
195
196     constraint_key = Constraint.EQUAL
197
198     valid_prop_types = Schema.PROPERTY_TYPES
199
200     def _is_valid(self, value):
201         if value == self.constraint_value:
202             return True
203
204         return False
205
206     def _err_msg(self, value):
207         return (_('The value "%(pvalue)s" of property "%(pname)s" is not '
208                   'equal to "%(cvalue)s".') %
209                 dict(pname=self.property_name,
210                      pvalue=self.value_msg,
211                      cvalue=self.constraint_value_msg))
212
213
214 class GreaterThan(Constraint):
215     """Constraint class for "greater_than"
216
217     Constrains a property or parameter to a value greater than ('>')
218     the value declared.
219     """
220
221     constraint_key = Constraint.GREATER_THAN
222
223     valid_types = (int, float, datetime.date,
224                    datetime.time, datetime.datetime)
225
226     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
227                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
228                         Schema.SCALAR_UNIT_TIME)
229
230     def __init__(self, property_name, property_type, constraint):
231         super(GreaterThan, self).__init__(property_name, property_type,
232                                           constraint)
233         if not isinstance(constraint[self.GREATER_THAN], self.valid_types):
234             ExceptionCollector.appendException(
235                 InvalidSchemaError(message=_('The property "greater_than" '
236                                              'expects comparable values.')))
237
238     def _is_valid(self, value):
239         if value > self.constraint_value:
240             return True
241
242         return False
243
244     def _err_msg(self, value):
245         return (_('The value "%(pvalue)s" of property "%(pname)s" must be '
246                   'greater than "%(cvalue)s".') %
247                 dict(pname=self.property_name,
248                      pvalue=self.value_msg,
249                      cvalue=self.constraint_value_msg))
250
251
252 class GreaterOrEqual(Constraint):
253     """Constraint class for "greater_or_equal"
254
255     Constrains a property or parameter to a value greater than or equal
256     to ('>=') the value declared.
257     """
258
259     constraint_key = Constraint.GREATER_OR_EQUAL
260
261     valid_types = (int, float, datetime.date,
262                    datetime.time, datetime.datetime)
263
264     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
265                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
266                         Schema.SCALAR_UNIT_TIME)
267
268     def __init__(self, property_name, property_type, constraint):
269         super(GreaterOrEqual, self).__init__(property_name, property_type,
270                                              constraint)
271         if not isinstance(self.constraint_value, self.valid_types):
272             ExceptionCollector.appendException(
273                 InvalidSchemaError(message=_('The property '
274                                              '"greater_or_equal" expects '
275                                              'comparable values.')))
276
277     def _is_valid(self, value):
278         if toscaparser.functions.is_function(value) or \
279            value >= self.constraint_value:
280             return True
281         return False
282
283     def _err_msg(self, value):
284         return (_('The value "%(pvalue)s" of property "%(pname)s" must be '
285                   'greater than or equal to "%(cvalue)s".') %
286                 dict(pname=self.property_name,
287                      pvalue=self.value_msg,
288                      cvalue=self.constraint_value_msg))
289
290
291 class LessThan(Constraint):
292     """Constraint class for "less_than"
293
294     Constrains a property or parameter to a value less than ('<')
295     the value declared.
296     """
297
298     constraint_key = Constraint.LESS_THAN
299
300     valid_types = (int, float, datetime.date,
301                    datetime.time, datetime.datetime)
302
303     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
304                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
305                         Schema.SCALAR_UNIT_TIME)
306
307     def __init__(self, property_name, property_type, constraint):
308         super(LessThan, self).__init__(property_name, property_type,
309                                        constraint)
310         if not isinstance(self.constraint_value, self.valid_types):
311             ExceptionCollector.appendException(
312                 InvalidSchemaError(message=_('The property "less_than" '
313                                              'expects comparable values.')))
314
315     def _is_valid(self, value):
316         if value < self.constraint_value:
317             return True
318
319         return False
320
321     def _err_msg(self, value):
322         return (_('The value "%(pvalue)s" of property "%(pname)s" must be '
323                   'less than "%(cvalue)s".') %
324                 dict(pname=self.property_name,
325                      pvalue=self.value_msg,
326                      cvalue=self.constraint_value_msg))
327
328
329 class LessOrEqual(Constraint):
330     """Constraint class for "less_or_equal"
331
332     Constrains a property or parameter to a value less than or equal
333     to ('<=') the value declared.
334     """
335
336     constraint_key = Constraint.LESS_OR_EQUAL
337
338     valid_types = (int, float, datetime.date,
339                    datetime.time, datetime.datetime)
340
341     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
342                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
343                         Schema.SCALAR_UNIT_TIME)
344
345     def __init__(self, property_name, property_type, constraint):
346         super(LessOrEqual, self).__init__(property_name, property_type,
347                                           constraint)
348         if not isinstance(self.constraint_value, self.valid_types):
349             ExceptionCollector.appendException(
350                 InvalidSchemaError(message=_('The property "less_or_equal" '
351                                              'expects comparable values.')))
352
353     def _is_valid(self, value):
354         if value <= self.constraint_value:
355             return True
356
357         return False
358
359     def _err_msg(self, value):
360         return (_('The value "%(pvalue)s" of property "%(pname)s" must be '
361                   'less than or equal to "%(cvalue)s".') %
362                 dict(pname=self.property_name,
363                      pvalue=self.value_msg,
364                      cvalue=self.constraint_value_msg))
365
366
367 class InRange(Constraint):
368     """Constraint class for "in_range"
369
370     Constrains a property or parameter to a value in range of (inclusive)
371     the two values declared.
372     """
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)