Merge "Support version of tosca_simple_yaml_1_1"
[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, STATUS
31     ) = (
32         'type', 'required', 'description',
33         'default', 'constraints', 'entry_schema', 'status'
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 status(self):
90         return self.schema.get(self.STATUS, '')
91
92     @property
93     def constraints(self):
94         if not self.constraints_list:
95             constraint_schemata = self.schema.get(self.CONSTRAINTS)
96             if constraint_schemata:
97                 self.constraints_list = [Constraint(self.name,
98                                                     self.type,
99                                                     cschema)
100                                          for cschema in constraint_schemata]
101         return self.constraints_list
102
103     @property
104     def entry_schema(self):
105         return self.schema.get(self.ENTRYSCHEMA)
106
107     def __getitem__(self, key):
108         return self.schema[key]
109
110     def __iter__(self):
111         for k in self.KEYS:
112             try:
113                 self.schema[k]
114             except KeyError:
115                 pass
116             else:
117                 yield k
118
119     def __len__(self):
120         if self._len is None:
121             self._len = len(list(iter(self)))
122         return self._len
123
124
125 class Constraint(object):
126     '''Parent class for constraints for a Property or Input.'''
127
128     CONSTRAINTS = (EQUAL, GREATER_THAN,
129                    GREATER_OR_EQUAL, LESS_THAN, LESS_OR_EQUAL, IN_RANGE,
130                    VALID_VALUES, LENGTH, MIN_LENGTH, MAX_LENGTH, PATTERN) = \
131                   ('equal', 'greater_than', 'greater_or_equal', 'less_than',
132                    'less_or_equal', 'in_range', 'valid_values', 'length',
133                    'min_length', 'max_length', 'pattern')
134
135     def __new__(cls, property_name, property_type, constraint):
136         if cls is not Constraint:
137             return super(Constraint, cls).__new__(cls)
138
139         if(not isinstance(constraint, collections.Mapping) or
140            len(constraint) != 1):
141             ExceptionCollector.appendException(
142                 InvalidSchemaError(message=_('Invalid constraint schema.')))
143
144         for type in constraint.keys():
145             ConstraintClass = get_constraint_class(type)
146             if not ConstraintClass:
147                 msg = _('Invalid property "%s".') % type
148                 ExceptionCollector.appendException(
149                     InvalidSchemaError(message=msg))
150
151         return ConstraintClass(property_name, property_type, constraint)
152
153     def __init__(self, property_name, property_type, constraint):
154         self.property_name = property_name
155         self.property_type = property_type
156         self.constraint_value = constraint[self.constraint_key]
157         self.constraint_value_msg = self.constraint_value
158         if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES:
159             self.constraint_value = self._get_scalarunit_constraint_value()
160         # check if constraint is valid for property type
161         if property_type not in self.valid_prop_types:
162             msg = _('Property "%(ctype)s" is not valid for data type '
163                     '"%(dtype)s".') % dict(
164                         ctype=self.constraint_key,
165                         dtype=property_type)
166             ExceptionCollector.appendException(InvalidSchemaError(message=msg))
167
168     def _get_scalarunit_constraint_value(self):
169         if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES:
170             ScalarUnit_Class = (scalarunit.
171                                 get_scalarunit_class(self.property_type))
172         if isinstance(self.constraint_value, list):
173             return [ScalarUnit_Class(v).get_num_from_scalar_unit()
174                     for v in self.constraint_value]
175         else:
176             return (ScalarUnit_Class(self.constraint_value).
177                     get_num_from_scalar_unit())
178
179     def _err_msg(self, value):
180         return _('Property "%s" could not be validated.') % self.property_name
181
182     def validate(self, value):
183         self.value_msg = value
184         if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES:
185             value = scalarunit.get_scalarunit_value(self.property_type, value)
186         if not self._is_valid(value):
187             err_msg = self._err_msg(value)
188             ExceptionCollector.appendException(
189                 ValidationError(message=err_msg))
190
191
192 class Equal(Constraint):
193     """Constraint class for "equal"
194
195     Constrains a property or parameter to a value equal to ('=')
196     the value declared.
197     """
198
199     constraint_key = Constraint.EQUAL
200
201     valid_prop_types = Schema.PROPERTY_TYPES
202
203     def _is_valid(self, value):
204         if value == self.constraint_value:
205             return True
206
207         return False
208
209     def _err_msg(self, value):
210         return (_('The value "%(pvalue)s" of property "%(pname)s" is not '
211                   'equal to "%(cvalue)s".') %
212                 dict(pname=self.property_name,
213                      pvalue=self.value_msg,
214                      cvalue=self.constraint_value_msg))
215
216
217 class GreaterThan(Constraint):
218     """Constraint class for "greater_than"
219
220     Constrains a property or parameter to a value greater than ('>')
221     the value declared.
222     """
223
224     constraint_key = Constraint.GREATER_THAN
225
226     valid_types = (int, float, datetime.date,
227                    datetime.time, datetime.datetime)
228
229     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
230                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
231                         Schema.SCALAR_UNIT_TIME)
232
233     def __init__(self, property_name, property_type, constraint):
234         super(GreaterThan, self).__init__(property_name, property_type,
235                                           constraint)
236         if not isinstance(constraint[self.GREATER_THAN], self.valid_types):
237             ExceptionCollector.appendException(
238                 InvalidSchemaError(message=_('The property "greater_than" '
239                                              'expects comparable values.')))
240
241     def _is_valid(self, value):
242         if value > self.constraint_value:
243             return True
244
245         return False
246
247     def _err_msg(self, value):
248         return (_('The value "%(pvalue)s" of property "%(pname)s" must be '
249                   'greater than "%(cvalue)s".') %
250                 dict(pname=self.property_name,
251                      pvalue=self.value_msg,
252                      cvalue=self.constraint_value_msg))
253
254
255 class GreaterOrEqual(Constraint):
256     """Constraint class for "greater_or_equal"
257
258     Constrains a property or parameter to a value greater than or equal
259     to ('>=') the value declared.
260     """
261
262     constraint_key = Constraint.GREATER_OR_EQUAL
263
264     valid_types = (int, float, datetime.date,
265                    datetime.time, datetime.datetime)
266
267     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
268                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
269                         Schema.SCALAR_UNIT_TIME)
270
271     def __init__(self, property_name, property_type, constraint):
272         super(GreaterOrEqual, self).__init__(property_name, property_type,
273                                              constraint)
274         if not isinstance(self.constraint_value, self.valid_types):
275             ExceptionCollector.appendException(
276                 InvalidSchemaError(message=_('The property '
277                                              '"greater_or_equal" expects '
278                                              'comparable values.')))
279
280     def _is_valid(self, value):
281         if toscaparser.functions.is_function(value) or \
282            value >= self.constraint_value:
283             return True
284         return False
285
286     def _err_msg(self, value):
287         return (_('The value "%(pvalue)s" of property "%(pname)s" must be '
288                   'greater than or equal to "%(cvalue)s".') %
289                 dict(pname=self.property_name,
290                      pvalue=self.value_msg,
291                      cvalue=self.constraint_value_msg))
292
293
294 class LessThan(Constraint):
295     """Constraint class for "less_than"
296
297     Constrains a property or parameter to a value less than ('<')
298     the value declared.
299     """
300
301     constraint_key = Constraint.LESS_THAN
302
303     valid_types = (int, float, datetime.date,
304                    datetime.time, datetime.datetime)
305
306     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
307                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
308                         Schema.SCALAR_UNIT_TIME)
309
310     def __init__(self, property_name, property_type, constraint):
311         super(LessThan, self).__init__(property_name, property_type,
312                                        constraint)
313         if not isinstance(self.constraint_value, self.valid_types):
314             ExceptionCollector.appendException(
315                 InvalidSchemaError(message=_('The property "less_than" '
316                                              'expects comparable values.')))
317
318     def _is_valid(self, value):
319         if value < self.constraint_value:
320             return True
321
322         return False
323
324     def _err_msg(self, value):
325         return (_('The value "%(pvalue)s" of property "%(pname)s" must be '
326                   'less than "%(cvalue)s".') %
327                 dict(pname=self.property_name,
328                      pvalue=self.value_msg,
329                      cvalue=self.constraint_value_msg))
330
331
332 class LessOrEqual(Constraint):
333     """Constraint class for "less_or_equal"
334
335     Constrains a property or parameter to a value less than or equal
336     to ('<=') the value declared.
337     """
338
339     constraint_key = Constraint.LESS_OR_EQUAL
340
341     valid_types = (int, float, datetime.date,
342                    datetime.time, datetime.datetime)
343
344     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
345                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
346                         Schema.SCALAR_UNIT_TIME)
347
348     def __init__(self, property_name, property_type, constraint):
349         super(LessOrEqual, self).__init__(property_name, property_type,
350                                           constraint)
351         if not isinstance(self.constraint_value, self.valid_types):
352             ExceptionCollector.appendException(
353                 InvalidSchemaError(message=_('The property "less_or_equal" '
354                                              'expects comparable values.')))
355
356     def _is_valid(self, value):
357         if value <= self.constraint_value:
358             return True
359
360         return False
361
362     def _err_msg(self, value):
363         return (_('The value "%(pvalue)s" of property "%(pname)s" must be '
364                   'less than or equal to "%(cvalue)s".') %
365                 dict(pname=self.property_name,
366                      pvalue=self.value_msg,
367                      cvalue=self.constraint_value_msg))
368
369
370 class InRange(Constraint):
371     """Constraint class for "in_range"
372
373     Constrains a property or parameter to a value in range of (inclusive)
374     the two values declared.
375     """
376     UNBOUNDED = 'UNBOUNDED'
377
378     constraint_key = Constraint.IN_RANGE
379
380     valid_types = (int, float, datetime.date,
381                    datetime.time, datetime.datetime, str)
382
383     valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP,
384                         Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY,
385                         Schema.SCALAR_UNIT_TIME, Schema.RANGE)
386
387     def __init__(self, property_name, property_type, constraint):
388         super(InRange, self).__init__(property_name, property_type, constraint)
389         if(not isinstance(self.constraint_value, collections.Sequence) or
390            (len(constraint[self.IN_RANGE]) != 2)):
391             ExceptionCollector.appendException(
392                 InvalidSchemaError(message=_('The property "in_range" '
393                                              'expects a list.')))
394
395         msg = _('The property "in_range" expects comparable values.')
396         for value in self.constraint_value:
397             if not isinstance(value, self.valid_types):
398                 ExceptionCollector.appendException(
399                     InvalidSchemaError(message=msg))
400             # The only string we allow for range is the special value
401             # 'UNBOUNDED'
402             if(isinstance(value, str) and value != self.UNBOUNDED):
403                 ExceptionCollector.appendException(
404                     InvalidSchemaError(message=msg))
405
406         self.min = self.constraint_value[0]
407         self.max = self.constraint_value[1]
408
409     def _is_valid(self, value):
410         if not isinstance(self.min, str):
411             if value < self.min:
412                 return False
413         elif self.min != self.UNBOUNDED:
414             return False
415         if not isinstance(self.max, str):
416             if value > self.max:
417                 return False
418         elif self.max != self.UNBOUNDED:
419             return False
420         return True
421
422     def _err_msg(self, value):
423         return (_('The value "%(pvalue)s" of property "%(pname)s" is out of '
424                   'range "(min:%(vmin)s, max:%(vmax)s)".') %
425                 dict(pname=self.property_name,
426                      pvalue=self.value_msg,
427                      vmin=self.constraint_value_msg[0],
428                      vmax=self.constraint_value_msg[1]))
429
430
431 class ValidValues(Constraint):
432     """Constraint class for "valid_values"
433
434     Constrains a property or parameter to a value that is in the list of
435     declared values.
436     """
437     constraint_key = Constraint.VALID_VALUES
438
439     valid_prop_types = Schema.PROPERTY_TYPES
440
441     def __init__(self, property_name, property_type, constraint):
442         super(ValidValues, self).__init__(property_name, property_type,
443                                           constraint)
444         if not isinstance(self.constraint_value, collections.Sequence):
445             ExceptionCollector.appendException(
446                 InvalidSchemaError(message=_('The property "valid_values" '
447                                              'expects a list.')))
448
449     def _is_valid(self, value):
450         if isinstance(value, list):
451             return all(v in self.constraint_value for v in value)
452         return value in self.constraint_value
453
454     def _err_msg(self, value):
455         allowed = '[%s]' % ', '.join(str(a) for a in self.constraint_value)
456         return (_('The value "%(pvalue)s" of property "%(pname)s" is not '
457                   'valid. Expected a value from "%(cvalue)s".') %
458                 dict(pname=self.property_name,
459                      pvalue=value,
460                      cvalue=allowed))
461
462
463 class Length(Constraint):
464     """Constraint class for "length"
465
466     Constrains the property or parameter to a value of a given length.
467     """
468
469     constraint_key = Constraint.LENGTH
470
471     valid_types = (int, )
472
473     valid_prop_types = (Schema.STRING, )
474
475     def __init__(self, property_name, property_type, constraint):
476         super(Length, self).__init__(property_name, property_type, constraint)
477         if not isinstance(self.constraint_value, self.valid_types):
478             ExceptionCollector.appendException(
479                 InvalidSchemaError(message=_('The property "length" expects '
480                                              'an integer.')))
481
482     def _is_valid(self, value):
483         if isinstance(value, str) and len(value) == self.constraint_value:
484             return True
485
486         return False
487
488     def _err_msg(self, value):
489         return (_('Length of value "%(pvalue)s" of property "%(pname)s" '
490                   'must be equal to "%(cvalue)s".') %
491                 dict(pname=self.property_name,
492                      pvalue=value,
493                      cvalue=self.constraint_value))
494
495
496 class MinLength(Constraint):
497     """Constraint class for "min_length"
498
499     Constrains the property or parameter to a value to a minimum length.
500     """
501
502     constraint_key = Constraint.MIN_LENGTH
503
504     valid_types = (int, )
505
506     valid_prop_types = (Schema.STRING, Schema.MAP)
507
508     def __init__(self, property_name, property_type, constraint):
509         super(MinLength, self).__init__(property_name, property_type,
510                                         constraint)
511         if not isinstance(self.constraint_value, self.valid_types):
512             ExceptionCollector.appendException(
513                 InvalidSchemaError(message=_('The property "min_length" '
514                                              'expects an integer.')))
515
516     def _is_valid(self, value):
517         if ((isinstance(value, str) or isinstance(value, dict)) and
518            len(value) >= self.constraint_value):
519             return True
520
521         return False
522
523     def _err_msg(self, value):
524         return (_('Length of value "%(pvalue)s" of property "%(pname)s" '
525                   'must be at least "%(cvalue)s".') %
526                 dict(pname=self.property_name,
527                      pvalue=value,
528                      cvalue=self.constraint_value))
529
530
531 class MaxLength(Constraint):
532     """Constraint class for "max_length"
533
534     Constrains the property or parameter to a value to a maximum length.
535     """
536
537     constraint_key = Constraint.MAX_LENGTH
538
539     valid_types = (int, )
540
541     valid_prop_types = (Schema.STRING, Schema.MAP)
542
543     def __init__(self, property_name, property_type, constraint):
544         super(MaxLength, self).__init__(property_name, property_type,
545                                         constraint)
546         if not isinstance(self.constraint_value, self.valid_types):
547             ExceptionCollector.appendException(
548                 InvalidSchemaError(message=_('The property "max_length" '
549                                              'expects an integer.')))
550
551     def _is_valid(self, value):
552         if ((isinstance(value, str) or isinstance(value, dict)) and
553            len(value) <= self.constraint_value):
554             return True
555
556         return False
557
558     def _err_msg(self, value):
559         return (_('Length of value "%(pvalue)s" of property "%(pname)s" '
560                   'must be no greater than "%(cvalue)s".') %
561                 dict(pname=self.property_name,
562                      pvalue=value,
563                      cvalue=self.constraint_value))
564
565
566 class Pattern(Constraint):
567     """Constraint class for "pattern"
568
569     Constrains the property or parameter to a value that is allowed by
570     the provided regular expression.
571     """
572
573     constraint_key = Constraint.PATTERN
574
575     valid_types = (str, )
576
577     valid_prop_types = (Schema.STRING, )
578
579     def __init__(self, property_name, property_type, constraint):
580         super(Pattern, self).__init__(property_name, property_type, constraint)
581         if not isinstance(self.constraint_value, self.valid_types):
582             ExceptionCollector.appendException(
583                 InvalidSchemaError(message=_('The property "pattern" '
584                                              'expects a string.')))
585         self.match = re.compile(self.constraint_value).match
586
587     def _is_valid(self, value):
588         match = self.match(value)
589         return match is not None and match.end() == len(value)
590
591     def _err_msg(self, value):
592         return (_('The value "%(pvalue)s" of property "%(pname)s" does not '
593                   'match pattern "%(cvalue)s".') %
594                 dict(pname=self.property_name,
595                      pvalue=value,
596                      cvalue=self.constraint_value))
597
598
599 constraint_mapping = {
600     Constraint.EQUAL: Equal,
601     Constraint.GREATER_THAN: GreaterThan,
602     Constraint.GREATER_OR_EQUAL: GreaterOrEqual,
603     Constraint.LESS_THAN: LessThan,
604     Constraint.LESS_OR_EQUAL: LessOrEqual,
605     Constraint.IN_RANGE: InRange,
606     Constraint.VALID_VALUES: ValidValues,
607     Constraint.LENGTH: Length,
608     Constraint.MIN_LENGTH: MinLength,
609     Constraint.MAX_LENGTH: MaxLength,
610     Constraint.PATTERN: Pattern
611     }
612
613
614 def get_constraint_class(type):
615     return constraint_mapping.get(type)