blob: ba94c7074a56690fde650b92885b4db6a9639f75 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
from aria.utils.collections import OrderedDict
from aria.utils.formatting import safe_repr
from aria.utils.type import full_type_name
from aria.utils.imports import import_fullname
from aria.parser import implements_specification
from aria.parser.presentation import (get_locator, validate_primitive)
from aria.parser.validation import Issue
from .functions import get_function
from ..presentation.types import get_type_by_full_or_shorthand_name
#
# DataType
#
def get_inherited_constraints(context, presentation):
"""
If we don't have constraints, will return our parent's constraints (if we have one),
recursively.
Implication: if we define even one constraint, the parent's constraints will not be inherited.
"""
constraints = presentation.constraints
if constraints is None:
# If we don't have any, use our parent's
parent = presentation._get_parent(context)
parent_constraints = get_inherited_constraints(context, parent) \
if parent is not None else None
if parent_constraints is not None:
constraints = parent_constraints
return constraints
def coerce_data_type_value(context, presentation, data_type, entry_schema, constraints, value, # pylint: disable=unused-argument
aspect):
"""
Handles the ``_coerce_data()`` hook for complex data types.
There are two kinds of handling:
1. If we have a primitive type as our great ancestor, then we do primitive type coersion, and
just check for constraints.
2. Otherwise, for normal complex data types we return the assigned property values while making
sure they are defined in our type. The property definition's default value, if available,
will be used if we did not assign it. We also make sure that required definitions indeed end
up with a value.
"""
primitive_type = data_type._get_primitive_ancestor(context)
if primitive_type is not None:
# Must be coercible to primitive ancestor
value = coerce_to_primitive(context, presentation, primitive_type, constraints, value,
aspect)
else:
definitions = data_type._get_properties(context)
if isinstance(value, dict):
temp = OrderedDict()
# Fill in our values, but make sure they are defined
for name, v in value.iteritems():
if name in definitions:
definition = definitions[name]
definition_type = definition._get_type(context)
definition_entry_schema = definition.entry_schema
definition_constraints = definition._get_constraints(context)
temp[name] = coerce_value(context, presentation, definition_type,
definition_entry_schema, definition_constraints, v,
aspect)
else:
context.validation.report(
'assignment to undefined property "%s" in type "%s" in "%s"'
% (name, data_type._fullname, presentation._fullname),
locator=get_locator(v, value, presentation), level=Issue.BETWEEN_TYPES)
# Fill in defaults from the definitions, and check if required definitions have not been
# assigned
for name, definition in definitions.iteritems():
if (temp.get(name) is None) and hasattr(definition, 'default') \
and (definition.default is not None):
definition_type = definition._get_type(context)
definition_entry_schema = definition.entry_schema
definition_constraints = definition._get_constraints(context)
temp[name] = coerce_value(context, presentation, definition_type,
definition_entry_schema, definition_constraints,
definition.default, 'default')
if getattr(definition, 'required', False) and (temp.get(name) is None):
context.validation.report(
'required property "%s" in type "%s" is not assigned a value in "%s"'
% (name, data_type._fullname, presentation._fullname),
locator=presentation._get_child_locator('definitions'),
level=Issue.BETWEEN_TYPES)
value = temp
elif value is not None:
context.validation.report('value of type "%s" is not a dict in "%s"'
% (data_type._fullname, presentation._fullname),
locator=get_locator(value, presentation),
level=Issue.BETWEEN_TYPES)
value = None
return value
def validate_data_type_name(context, presentation):
"""
Makes sure the complex data type's name is not that of a built-in type.
"""
name = presentation._name
if get_primitive_data_type(name) is not None:
context.validation.report('data type name is that of a built-in type: %s'
% safe_repr(name),
locator=presentation._locator, level=Issue.BETWEEN_TYPES)
#
# PropertyDefinition, AttributeDefinition, EntrySchema, DataType
#
def get_data_type(context, presentation, field_name, allow_none=False):
"""
Returns the type, whether it's a complex data type (a DataType instance) or a primitive (a
Python primitive type class).
If the type is not specified, defaults to :class:`str`, per note in section 3.2.1.1 of the
`TOSCA Simple Profile v1.0 cos01 specification <http://docs.oasis-open.org/tosca
/TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html
#_Toc379455072>`__
"""
type_name = getattr(presentation, field_name)
if type_name is None:
if allow_none:
return None
else:
return str
# Make sure not derived from self
if type_name == presentation._name:
return None
# Avoid circular definitions
container_data_type = get_container_data_type(presentation)
if (container_data_type is not None) and (container_data_type._name == type_name):
return None
# Try complex data type
data_type = get_type_by_full_or_shorthand_name(context, type_name, 'data_types')
if data_type is not None:
return data_type
# Try primitive data type
return get_primitive_data_type(type_name)
#
# PropertyDefinition, EntrySchema
#
def get_property_constraints(context, presentation):
"""
If we don't have constraints, will return our type's constraints (if we have one), recursively.
Implication: if we define even one constraint, the type's constraints will not be inherited.
"""
constraints = presentation.constraints
if constraints is None:
# If we don't have any, use our type's
the_type = presentation._get_type(context)
type_constraints = the_type._get_constraints(context) \
if hasattr(the_type, '_get_constraints') else None
if type_constraints is not None:
constraints = type_constraints
return constraints
#
# ConstraintClause
#
def apply_constraint_to_value(context, presentation, constraint_clause, value): # pylint: disable=too-many-statements,too-many-return-statements,too-many-branches
"""
Returns false if the value does not conform to the constraint.
"""
constraint_key = constraint_clause._raw.keys()[0]
the_type = constraint_clause._get_type(context)
# PropertyAssignment does not have this:
entry_schema = getattr(presentation, 'entry_schema', None)
def coerce_constraint(constraint):
return coerce_value(context, presentation, the_type, entry_schema, None, constraint,
constraint_key)
def report(message, constraint):
context.validation.report('value %s %s per constraint in "%s": %s'
% (message, safe_repr(constraint),
presentation._name or presentation._container._name,
safe_repr(value)),
locator=presentation._locator, level=Issue.BETWEEN_FIELDS)
if constraint_key == 'equal':
constraint = coerce_constraint(constraint_clause.equal)
if value != constraint:
report('is not equal to', constraint)
return False
elif constraint_key == 'greater_than':
constraint = coerce_constraint(constraint_clause.greater_than)
if value <= constraint:
report('is not greater than', constraint)
return False
elif constraint_key == 'greater_or_equal':
constraint = coerce_constraint(constraint_clause.greater_or_equal)
if value < constraint:
report('is not greater than or equal to', constraint)
return False
elif constraint_key == 'less_than':
constraint = coerce_constraint(constraint_clause.less_than)
if value >= constraint:
report('is not less than', constraint)
return False
elif constraint_key == 'less_or_equal':
constraint = coerce_constraint(constraint_clause.less_or_equal)
if value > constraint:
report('is not less than or equal to', constraint)
return False
elif constraint_key == 'in_range':
lower, upper = constraint_clause.in_range
lower, upper = coerce_constraint(lower), coerce_constraint(upper)
if value < lower:
report('is not greater than or equal to lower bound', lower)
return False
if (upper != 'UNBOUNDED') and (value > upper):
report('is not lesser than or equal to upper bound', upper)
return False
elif constraint_key == 'valid_values':
constraint = tuple(coerce_constraint(v) for v in constraint_clause.valid_values)
if value not in constraint:
report('is not one of', constraint)
return False
elif constraint_key == 'length':
constraint = constraint_clause.length
try:
if len(value) != constraint:
report('is not of length', constraint)
return False
except TypeError:
pass # should be validated elsewhere
elif constraint_key == 'min_length':
constraint = constraint_clause.min_length
try:
if len(value) < constraint:
report('has a length lesser than', constraint)
return False
except TypeError:
pass # should be validated elsewhere
elif constraint_key == 'max_length':
constraint = constraint_clause.max_length
try:
if len(value) > constraint:
report('has a length greater than', constraint)
return False
except TypeError:
pass # should be validated elsewhere
elif constraint_key == 'pattern':
constraint = constraint_clause.pattern
try:
# From TOSCA 1.0 3.5.2.1:
#
# "Note: Future drafts of this specification will detail the use of regular expressions
# and reference an appropriate standardized grammar."
#
# So we will just use Python's.
if re.match(constraint, str(value)) is None:
report('does not match regular expression', constraint)
return False
except re.error:
pass # should be validated elsewhere
return True
#
# Repository
#
def get_data_type_value(context, presentation, field_name, type_name):
the_type = get_type_by_full_or_shorthand_name(context, type_name, 'data_types')
if the_type is not None:
value = getattr(presentation, field_name)
if value is not None:
return coerce_data_type_value(context, presentation, the_type, None, None, value, None)
else:
context.validation.report('field "%s" in "%s" refers to unknown data type "%s"'
% (field_name, presentation._fullname, type_name),
locator=presentation._locator, level=Issue.BETWEEN_TYPES)
return None
#
# Utils
#
PRIMITIVE_DATA_TYPES = {
# YAML 1.2:
'tag:yaml.org,2002:str': unicode,
'tag:yaml.org,2002:integer': int,
'tag:yaml.org,2002:float': float,
'tag:yaml.org,2002:bool': bool,
'tag:yaml.org,2002:null': None.__class__,
# TOSCA aliases:
'string': unicode,
'integer': int,
'float': float,
'boolean': bool,
'null': None.__class__}
@implements_specification('3.2.1-3', 'tosca-simple-1.0')
def get_primitive_data_type(type_name):
"""
Many of the types we use in this profile are built-in types from the YAML 1.2 specification
(i.e., those identified by the "tag:yaml.org,2002" version tag) [YAML-1.2].
See the `TOSCA Simple Profile v1.0 cos01 specification <http://docs.oasis-open.org/tosca
/TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html
#_Toc373867862>`__
"""
return PRIMITIVE_DATA_TYPES.get(type_name)
def get_data_type_name(the_type):
"""
Returns the name of the type, whether it's a DataType, a primitive type, or another class.
"""
return the_type._name if hasattr(the_type, '_name') else full_type_name(the_type)
def coerce_value(context, presentation, the_type, entry_schema, constraints, value, aspect=None): # pylint: disable=too-many-return-statements
"""
Returns the value after it's coerced to its type, reporting validation errors if it cannot be
coerced.
Supports both complex data types and primitives.
Data types can use the ``coerce_value`` extension to hook their own specialized function.
If the extension is present, we will delegate to that hook.
"""
# TODO: should support models as well as presentations
is_function, func = get_function(context, presentation, value)
if is_function:
return func
if the_type is None:
return value
if the_type == None.__class__:
if value is not None:
context.validation.report('field "%s" is of type "null" but has a non-null value: %s'
% (presentation._name, safe_repr(value)),
locator=presentation._locator, level=Issue.BETWEEN_FIELDS)
return None
# Delegate to 'coerce_value' extension
if hasattr(the_type, '_get_extension'):
coerce_value_fn_name = the_type._get_extension('coerce_value')
if coerce_value_fn_name is not None:
if value is None:
return None
coerce_value_fn = import_fullname(coerce_value_fn_name)
return coerce_value_fn(context, presentation, the_type, entry_schema, constraints,
value, aspect)
if hasattr(the_type, '_coerce_value'):
# Delegate to '_coerce_value' (likely a DataType instance)
return the_type._coerce_value(context, presentation, entry_schema, constraints, value,
aspect)
# Coerce to primitive type
return coerce_to_primitive(context, presentation, the_type, constraints, value, aspect)
def coerce_to_primitive(context, presentation, primitive_type, constraints, value, aspect=None):
"""
Returns the value after it's coerced to a primitive type, translating exceptions to validation
errors if it cannot be coerced.
"""
if value is None:
return None
try:
# Coerce
value = validate_primitive(value, primitive_type,
context.validation.allow_primitive_coersion)
# Check constraints
apply_constraints_to_value(context, presentation, constraints, value)
except ValueError as e:
report_issue_for_bad_format(context, presentation, primitive_type, value, aspect, e)
value = None
except TypeError as e:
report_issue_for_bad_format(context, presentation, primitive_type, value, aspect, e)
value = None
return value
def coerce_to_data_type_class(context, presentation, cls, entry_schema, constraints, value,
aspect=None):
"""
Returns the value after it's coerced to a data type class, reporting validation errors if it
cannot be coerced. Constraints will be applied after coersion.
Will either call a ``_create`` static function in the class, or instantiate it using a
constructor if ``_create`` is not available.
This will usually be called by a ``coerce_value`` extension hook in a :class:`DataType`.
"""
try:
if hasattr(cls, '_create'):
# Instantiate using creator function
value = cls._create(context, presentation, entry_schema, constraints, value, aspect)
else:
# Normal instantiation
value = cls(entry_schema, constraints, value, aspect)
except ValueError as e:
report_issue_for_bad_format(context, presentation, cls, value, aspect, e)
value = None
# Check constraints
value = apply_constraints_to_value(context, presentation, constraints, value)
return value
def apply_constraints_to_value(context, presentation, constraints, value):
"""
Applies all constraints to the value. If the value conforms, returns the value. If it does not
conform, returns None.
"""
if (value is not None) and (constraints is not None):
valid = True
for constraint in constraints:
if not constraint._apply_to_value(context, presentation, value):
valid = False
if not valid:
value = None
return value
def get_container_data_type(presentation):
if presentation is None:
return None
if type(presentation).__name__ == 'DataType':
return presentation
return get_container_data_type(presentation._container)
def report_issue_for_bad_format(context, presentation, the_type, value, aspect, e):
if aspect == 'default':
aspect = '"default" value'
elif aspect is not None:
aspect = '"%s" aspect' % aspect
if aspect is not None:
context.validation.report('%s for field "%s" is not a valid "%s": %s'
% (aspect, presentation._name or presentation._container._name,
get_data_type_name(the_type), safe_repr(value)),
locator=presentation._locator, level=Issue.BETWEEN_FIELDS,
exception=e)
else:
context.validation.report('field "%s" is not a valid "%s": %s'
% (presentation._name or presentation._container._name,
get_data_type_name(the_type), safe_repr(value)),
locator=presentation._locator, level=Issue.BETWEEN_FIELDS,
exception=e)